diff options
867 files changed, 17915 insertions, 6748 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 7fd35d8241..139ee8013b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -23,7 +23,7 @@ checks: engines: rubocop: enabled: true - channel: rubocop-0-58 + channel: rubocop-0-60 ratings: paths: diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml new file mode 100644 index 0000000000..d73b2e3362 --- /dev/null +++ b/.github/autolabeler.yml @@ -0,0 +1,24 @@ +actioncable: + - "actioncable/**/*" +actionmailer: + - "actionmailer/**/*" +actionpack: + - "actionpack/**/*" +actionview: + - "actionview/**/*" +activejob: + - "activejob/**/*" +activemodel: + - "activemodel/**/*" +activerecord: + - "activerecord/**/*" +activestorage: + - "activestorage/**/*" +activesupport: + - "activesupport/**/*" +rails-ujs: + - "actionview/app/assets/javascripts/rails-ujs*/*" +railties: + - "railties/**/*" +docs: + - "guides/**/*" diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000000..326fa84b7e --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,12 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 14 +# Label requiring a response +responseRequiredLabel: more-information-needed +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no follow-up + response from the original author. We currently don't have enough + information in order to take action. Please reach out if you have any additional + information that will help us move this issue forward. diff --git a/.gitignore b/.gitignore index ab9ca8a6ac..35440cb54f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ debug.log node_modules/ package-lock.json pkg/ +/tmp/ +/yarn-error.log +/yarn.lock diff --git a/.rubocop.yml b/.rubocop.yml index 3e15d34dbd..033f7adf49 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,7 @@ AllCops: - '**/vendor/**/*' - 'actionpack/lib/action_dispatch/journey/parser.rb' - 'railties/test/fixtures/tmp/**/*' + - 'node_modules/**/*' Performance: Exclude: @@ -130,6 +131,9 @@ Style/FrozenStringLiteralComment: - 'actionpack/test/**/*.ruby' - 'activestorage/db/migrate/**/*.rb' +Style/RedundantFreeze: + Enabled: true + # Use `foo {}` not `foo{}`. Layout/SpaceBeforeBlockBraces: Enabled: true @@ -137,6 +141,7 @@ Layout/SpaceBeforeBlockBraces: # Use `foo { bar }` not `foo {bar}`. Layout/SpaceInsideBlockBraces: Enabled: true + EnforcedStyleForEmptyBraces: space # Use `{ a: 1 }` not `{a:1}`. Layout/SpaceInsideHashLiteralBraces: @@ -173,6 +178,12 @@ Lint/RequireParentheses: Lint/StringConversionInInterpolation: Enabled: true +Lint/UriEscapeUnescape: + Enabled: true + +Style/ParenthesesAroundCondition: + Enabled: true + Style/RedundantReturn: Enabled: true AllowMultipleReturnValues: true @@ -202,3 +213,6 @@ Performance/EndWith: Performance/RegexpMatch: Enabled: true + +Performance/UnfreezeString: + Enabled: true diff --git a/.travis.yml b/.travis.yml index d7544fecb6..3db4e9cd94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,29 +14,34 @@ services: - redis-server addons: - postgresql: "9.6" + postgresql: 10 chrome: stable apt: sources: - sourceline: "ppa:mc3man/trusty-media" - sourceline: "ppa:ubuntuhandbook1/apps" + - mysql-5.7-trusty packages: - ffmpeg - mupdf - mupdf-tools - poppler-utils + - mysql-server + - mysql-client + - postgresql-10 + - postgresql-client-10 -bundler_args: --without test --jobs 3 --retry 3 +bundler_args: --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" - "travis_retry gem update --system" - "travis_retry gem install bundler" - - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" - "[[ $GEM != 'av:ujs' ]] || nvm install node" - "[[ $GEM != 'av:ujs' ]] || node --version" - "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)" + - "[[ $GEM != 'railties' ]] || (curl -o- -L https://yarnpkg.com/install.sh | bash)" + - "[[ $GEM != 'railties' ]] || export PATH=$HOME/.yarn/bin:$PATH" before_script: # Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted. @@ -50,13 +55,10 @@ env: global: - "JRUBY_OPTS='--dev -J-Xmx1024M'" matrix: - - "GEM=railties" - "GEM=ap,ac" - "GEM=am,amo,as,av,aj,ast" - "GEM=as PRESERVE_TIMEZONES=1" - - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - - "GEM=ar:postgresql" - "GEM=guides" - "GEM=ac:integration" @@ -67,26 +69,95 @@ rvm: matrix: include: + - rvm: 2.4.4 + env: "GEM=railties" + sudo: required + before_install: + - "rm ${BUNDLE_GEMFILE}.lock" + - "travis_retry gem update --system" + - "travis_retry gem install bundler" + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - rvm: 2.5.1 + env: "GEM=railties" + sudo: required + before_install: + - "rm ${BUNDLE_GEMFILE}.lock" + - "travis_retry gem update --system" + - "travis_retry gem install bundler" + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - rvm: ruby-head + env: "GEM=railties" + sudo: required + before_install: + - "rm ${BUNDLE_GEMFILE}.lock" + - "travis_retry gem update --system" + - "travis_retry gem install bundler" + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" - rvm: 2.5.1 env: "GEM=av:ujs" - rvm: 2.4.4 + sudo: required env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq + before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: 2.5.1 + sudo: required env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq + before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: ruby-head + sudo: required env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq + before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" + - rvm: 2.4.4 + env: "GEM=ar:mysql2" + sudo: required + before_install: + - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"" + - "sudo mysql_upgrade" + - "sudo service mysql restart" + - rvm: 2.5.1 + env: "GEM=ar:mysql2" + sudo: required + before_install: + - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"" + - "sudo mysql_upgrade" + - "sudo service mysql restart" + - rvm: ruby-head + env: "GEM=ar:mysql2" + sudo: required + before_install: + - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"" + - "sudo mysql_upgrade" + - "sudo service mysql restart" - rvm: 2.5.1 env: - "GEM=ar:mysql2 MYSQL=mariadb" @@ -95,6 +166,24 @@ matrix: - rvm: 2.5.1 env: - "GEM=ar:sqlite3_mem" + - rvm: 2.4.4 + env: "GEM=ar:postgresql" + sudo: required + before_install: + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - rvm: 2.5.1 + env: "GEM=ar:postgresql" + sudo: required + before_install: + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - rvm: ruby-head + env: "GEM=ar:postgresql" + sudo: required + before_install: + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" - rvm: 2.5.1 env: - "GEM=ar:postgresql POSTGRES=9.2" diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000000..6be74e145d --- /dev/null +++ b/.yarnrc @@ -0,0 +1,2 @@ +workspaces-experimental true +--add.prefer-offline true
\ No newline at end of file @@ -13,3 +13,4 @@ brew "yarn" cask "xquartz" brew "mupdf" brew "poppler" +brew "imagemagick" @@ -9,15 +9,12 @@ gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. gem "rake", ">= 11.1" -gem "mocha" - gem "capybara", ">= 2.15" gem "rack-cache", "~> 1.2" -gem "coffee-rails" gem "sass-rails" gem "turbolinks", "~> 5" - +gem "webpacker", github: "rails/webpacker", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true" # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid Active Model (and by extension the entire framework) # being dependent on a binary library. @@ -89,7 +86,6 @@ group :storage do gem "azure-storage", require: false gem "image_processing", "~> 1.2" - gem "ffi", "<= 1.9.21" end group :ujs do @@ -103,6 +99,7 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do gem "minitest-bisect" + gem "minitest-retry" platforms :mri do gem "stackprof" diff --git a/Gemfile.lock b/Gemfile.lock index 259221979e..c362d7cedf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,6 +30,15 @@ GIT queue_classic (3.2.0.RC1) pg (>= 0.17, < 2.0) +GIT + remote: https://github.com/rails/webpacker.git + revision: 48d9fd52c3e9637dbb21e551b48c84c0f2dbb2ed + specs: + webpacker (4.0.0.pre.pre.2) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) + railties (>= 4.2) + PATH remote: . specs: @@ -96,16 +105,16 @@ PATH GEM remote: https://rubygems.org/ specs: - activerecord-jdbc-adapter (51.1-java) - activerecord (~> 5.1.0) - activerecord-jdbcmysql-adapter (51.1-java) - activerecord-jdbc-adapter (= 51.1) + activerecord-jdbc-adapter (52.0-java) + activerecord (~> 5.2.0) + activerecord-jdbcmysql-adapter (52.0-java) + activerecord-jdbc-adapter (= 52.0) jdbc-mysql (~> 5.1.36) - activerecord-jdbcpostgresql-adapter (51.1-java) - activerecord-jdbc-adapter (= 51.1) + activerecord-jdbcpostgresql-adapter (52.0-java) + activerecord-jdbc-adapter (= 52.0) jdbc-postgres (>= 9.4, < 43) - activerecord-jdbcsqlite3-adapter (51.1-java) - activerecord-jdbc-adapter (= 51.1) + activerecord-jdbcsqlite3-adapter (52.0-java) + activerecord-jdbc-adapter (= 52.0) jdbc-sqlite3 (~> 3.8, < 3.30) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) @@ -113,21 +122,21 @@ GEM archive-zip (0.11.0) io-like (~> 0.3.0) ast (2.4.0) - aws-eventstream (1.0.0) - aws-partitions (1.88.0) - aws-sdk-core (3.21.2) + aws-eventstream (1.0.1) + aws-partitions (1.102.0) + aws-sdk-core (3.25.0) aws-eventstream (~> 1.0) aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-kms (1.5.0) + aws-sdk-kms (1.7.0) aws-sdk-core (~> 3) aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.13.0) + aws-sdk-s3 (1.17.1) aws-sdk-core (~> 3, >= 3.21.2) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) - aws-sigv4 (1.0.2) + aws-sigv4 (1.0.3) azure-core (0.1.14) faraday (~> 0.9) faraday_middleware (~> 0.10) @@ -137,9 +146,9 @@ GEM faraday (~> 0.9) faraday_middleware (~> 0.10) nokogiri (~> 1.6, >= 1.6.8) - backburner (1.4.1) + backburner (1.5.0) beaneater (~> 1.0) - concurrent-ruby (~> 1.0.1) + concurrent-ruby (~> 1.0, >= 1.0.1) dante (> 0.1.5) bcrypt (3.1.12) bcrypt (3.1.12-java) @@ -164,29 +173,26 @@ GEM childprocess faraday selenium-webdriver - bootsnap (1.3.0) + bootsnap (1.3.1) msgpack (~> 1.0) - bootsnap (1.3.0-java) + bootsnap (1.3.1-java) msgpack (~> 1.0) builder (3.2.3) bunny (2.9.2) amq-protocol (~> 2.3.0) byebug (10.0.2) - capybara (3.1.1) + capybara (3.7.1) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - xpath (~> 3.0) + xpath (~> 3.1) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) - chromedriver-helper (1.2.0) + chromedriver-helper (2.0.0) archive-zip (~> 0.10) nokogiri (~> 1.8) - coffee-rails (4.2.2) - coffee-script (>= 2.2.0) - railties (>= 4.0.0) coffee-script (2.4.1) coffee-script-source execjs @@ -217,7 +223,7 @@ GEM em-socksify (0.3.2) eventmachine (>= 1.0.0.beta.4) erubi (1.7.1) - et-orbi (1.1.2) + et-orbi (1.1.6) tzinfo event_emitter (0.2.6) eventmachine (1.2.7) @@ -237,48 +243,48 @@ GEM faye-websocket (0.10.7) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.21) - ffi (1.9.21-java) - ffi (1.9.21-x64-mingw32) - ffi (1.9.21-x86-mingw32) - fugit (1.1.3) - et-orbi (~> 1.1, >= 1.1.1) + ffi (1.9.25) + ffi (1.9.25-java) + ffi (1.9.25-x64-mingw32) + ffi (1.9.25-x86-mingw32) + fugit (1.1.6) + et-orbi (~> 1.1, >= 1.1.6) raabro (~> 1.1) globalid (0.4.1) activesupport (>= 4.2.0) - google-api-client (0.19.8) + google-api-client (0.23.8) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.5, < 0.7.0) httpclient (>= 2.8.1, < 3.0) mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - google-cloud-core (1.2.0) + signet (~> 0.9) + google-cloud-core (1.2.3) google-cloud-env (~> 1.0) - google-cloud-env (1.0.1) + google-cloud-env (1.0.2) faraday (~> 0.11) - google-cloud-storage (1.12.0) + google-cloud-storage (1.13.1) digest-crc (~> 0.4) - google-api-client (~> 0.19.0) + google-api-client (~> 0.23) google-cloud-core (~> 1.2) googleauth (~> 0.6.2) - googleauth (0.6.2) + googleauth (0.6.6) faraday (~> 0.12) jwt (>= 1.4, < 3.0) - logging (~> 2.0) memoist (~> 0.12) multi_json (~> 1.11) - os (~> 0.9) + os (>= 0.9, < 2.0) signet (~> 0.7) hiredis (0.6.1) hiredis (0.6.1-java) http_parser.rb (0.6.0) httpclient (2.8.3) - i18n (1.0.1) + i18n (1.1.0) concurrent-ruby (~> 1.0) - image_processing (1.2.0) + image_processing (1.6.0) mini_magick (~> 4.0) - ruby-vips (>= 2.0.10, < 3) + ruby-vips (>= 2.0.11, < 3) io-like (0.3.0) jaro_winkler (1.5.1) jaro_winkler (1.5.1-java) @@ -297,35 +303,30 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - little-plugger (1.1.4) - logging (2.2.2) - little-plugger (~> 1.1) - multi_json (~> 1.10) loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.0) + mail (2.7.1) mini_mime (>= 0.1.1) - marcel (0.3.2) + marcel (0.3.3) mimemagic (~> 0.3.2) memoist (0.16.0) - metaclass (0.0.4) method_source (0.9.0) - mime-types (3.1) + mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types-data (3.2018.0812) mimemagic (0.3.2) - mini_magick (4.8.0) - mini_mime (1.0.0) + mini_magick (4.9.2) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.11.3) minitest-bisect (1.4.0) minitest-server (~> 1.0) path_expander (~> 1.0) + minitest-retry (0.1.9) + minitest (>= 5.0) minitest-server (1.0.5) minitest (~> 5.0) - mocha (1.5.0) - metaclass (~> 0.0.1) mono_logger (1.1.0) msgpack (1.2.4) msgpack (1.2.4-java) @@ -334,32 +335,32 @@ GEM multi_json (1.13.1) multipart-post (2.0.0) mustache (1.0.5) - mustermann (1.0.2) - mysql2 (0.5.1) - mysql2 (0.5.1-x64-mingw32) - mysql2 (0.5.1-x86-mingw32) + mustermann (1.0.3) + mysql2 (0.5.2) + mysql2 (0.5.2-x64-mingw32) + mysql2 (0.5.2-x86-mingw32) nio4r (2.3.1) nio4r (2.3.1-java) - nokogiri (1.8.2) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) - nokogiri (1.8.2-java) - nokogiri (1.8.2-x64-mingw32) + nokogiri (1.8.5-java) + nokogiri (1.8.5-x64-mingw32) mini_portile2 (~> 2.3.0) - nokogiri (1.8.2-x86-mingw32) + nokogiri (1.8.5-x86-mingw32) mini_portile2 (~> 2.3.0) - os (0.9.6) + os (1.0.0) parallel (1.12.1) - parser (2.5.1.2) + parser (2.5.3.0) ast (~> 2.4.0) path_expander (1.0.3) - pg (1.0.0) - pg (1.0.0-x64-mingw32) - pg (1.0.0-x86-mingw32) + pg (1.1.3) + pg (1.1.3-x64-mingw32) + pg (1.1.3-x86-mingw32) powerpack (0.1.2) psych (3.0.2) - public_suffix (3.0.2) - puma (3.11.4) - puma (3.11.4-java) + public_suffix (3.0.3) + puma (3.12.0) + puma (3.12.0-java) que (0.14.3) qunit-selenium (0.0.4) selenium-webdriver @@ -367,11 +368,13 @@ GEM raabro (1.1.6) racc (1.4.14) rack (2.0.5) - rack-cache (1.7.2) + rack-cache (1.8.0) rack (>= 0.4) - rack-protection (2.0.1) + rack-protection (2.0.3) rack - rack-test (1.0.0) + rack-proxy (0.6.5) + rack + rack-test (1.1.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -383,7 +386,7 @@ GEM rb-fsevent (0.10.3) rdoc (6.0.4) redcarpet (3.2.3) - redis (4.0.1) + redis (4.0.2) redis-namespace (1.6.0) redis (>= 3.0.4) representable (3.0.4) @@ -401,23 +404,23 @@ GEM redis (>= 3.3, < 5) resque (~> 1.26) rufus-scheduler (~> 3.2) - retriable (3.1.1) - rubocop (0.58.2) + retriable (3.1.2) + rubocop (0.60.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.9.0) - ruby-vips (2.0.12) + unicode-display_width (~> 1.4.0) + ruby-progressbar (1.10.0) + ruby-vips (2.0.13) ffi (~> 1.9) ruby_dep (1.5.0) - rubyzip (1.2.1) - rufus-scheduler (3.5.0) - fugit (~> 1.1, >= 1.1.1) - sass (3.5.6) + rubyzip (1.2.2) + rufus-scheduler (3.5.2) + fugit (~> 1.1, >= 1.1.5) + sass (3.5.7) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -430,27 +433,26 @@ GEM tilt (>= 1.1, < 3) sdoc (1.0.0) rdoc (>= 5.0) - selenium-webdriver (3.12.0) + selenium-webdriver (3.14.0) childprocess (~> 0.5) rubyzip (~> 1.2) - sequel (5.8.0) - serverengine (2.0.6) + sequel (5.12.0) + serverengine (2.0.7) sigdump (~> 0.2.2) - sidekiq (5.1.3) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) + sidekiq (5.2.1) + connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) sigdump (0.2.4) - signet (0.8.1) + signet (0.9.1) addressable (~> 2.3) faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sinatra (2.0.1) + sinatra (2.0.3) mustermann (~> 1.0) rack (~> 2.0) - rack-protection (= 2.0.1) + rack-protection (= 2.0.3) tilt (~> 2.0) sneakers (2.7.0) bunny (~> 2.9.2) @@ -468,9 +470,9 @@ GEM sqlite3 (1.3.13) sqlite3 (1.3.13-x64-mingw32) sqlite3 (1.3.13-x86-mingw32) - stackprof (0.2.11) - sucker_punch (2.0.4) - concurrent-ruby (~> 1.0.0) + stackprof (0.2.12) + sucker_punch (2.1.1) + concurrent-ruby (~> 1.0) thin (1.7.2) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) @@ -478,21 +480,21 @@ GEM thread_safe (0.3.6) thread_safe (0.3.6-java) tilt (2.0.8) - turbolinks (5.1.1) - turbolinks-source (~> 5.1) - turbolinks-source (5.1.0) + turbolinks (5.2.0) + turbolinks-source (~> 5.2) + turbolinks-source (5.2.0) tzinfo (1.2.5) thread_safe (~> 0.1) tzinfo-data (1.2018.5) tzinfo (>= 1.0.0) uber (0.1.0) - uglifier (4.1.10) + uglifier (4.1.18) execjs (>= 0.3.0, < 3) unicode-display_width (1.4.0) useragent (0.16.10) vegas (0.1.11) rack (>= 1.0.0) - w3c_validators (1.3.3) + w3c_validators (1.3.4) json (>= 1.8) nokogiri (~> 1.6) wdm (0.1.1) @@ -526,12 +528,10 @@ DEPENDENCIES byebug capybara (>= 2.15) chromedriver-helper - coffee-rails connection_pool dalli delayed_job delayed_job_active_record - ffi (<= 1.9.21) google-cloud-storage (~> 1.11) hiredis image_processing (~> 1.2) @@ -540,7 +540,7 @@ DEPENDENCIES libxml-ruby listen (>= 3.0.5, < 3.2) minitest-bisect - mocha + minitest-retry mysql2 (>= 0.4.10) nokogiri (>= 1.8.1) pg (>= 0.18.0) @@ -575,7 +575,8 @@ DEPENDENCIES uglifier (>= 1.3.0) w3c_validators wdm (>= 0.1.0) + webpacker! websocket-client-simple! BUNDLED WITH - 1.16.2 + 1.16.6 diff --git a/actioncable/.babelrc b/actioncable/.babelrc new file mode 100644 index 0000000000..4f0c469c60 --- /dev/null +++ b/actioncable/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + ["env", { "modules": false, "loose": true } ] + ], + "plugins": [ + "external-helpers" + ] +} diff --git a/actioncable/.eslintrc b/actioncable/.eslintrc new file mode 100644 index 0000000000..3d9ecd4bce --- /dev/null +++ b/actioncable/.eslintrc @@ -0,0 +1,19 @@ +{ + "extends": "eslint:recommended", + "rules": { + "semi": ["error", "never"], + "quotes": ["error", "double"], + "no-unused-vars": ["error", { "vars": "all", "args": "none" }] + }, + "plugins": [ + "import" + ], + "env": { + "browser": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + } +} diff --git a/actioncable/.gitignore b/actioncable/.gitignore index f514e58c16..5d5c867f2d 100644 --- a/actioncable/.gitignore +++ b/actioncable/.gitignore @@ -1,2 +1,3 @@ +/app/javascript/action_cable/internal.js /lib/assets/compiled/ /tmp/ diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 959943016f..17c7e9a3b7 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,21 @@ +* Add `id` option to redis adapter so now you can distinguish + ActionCable's redis connections among others. Also, you can set + custom id in options. + + Before: + ``` + $ redis-cli client list + id=669 addr=127.0.0.1:46442 fd=8 name= age=18 ... + ``` + + After: + ``` + $ redis-cli client list + id=673 addr=127.0.0.1:46516 fd=8 name=ActionCable-PID-19413 age=2 ... + ``` + + *Ilia Kasianenko* + * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/actioncable/README.md b/actioncable/README.md index a05ef1dd20..a2783d6f45 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -425,7 +425,7 @@ The above will start a cable server on port 28080. ### In app -If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`: +If you are using a server that supports the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`: ```ruby # config/application.rb @@ -442,7 +442,7 @@ Beware that currently, the cable server will _not_ auto-reload any changes in th We'll get all this abstracted properly when the framework is integrated into Rails. -The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). +The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/). ## Dependencies @@ -459,7 +459,7 @@ support, which means you can use all your regular Rails models with no problems as long as you haven't committed any thread-safety sins. The Action Cable server does _not_ need to be a multi-threaded application server. -This is because Action Cable uses the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking) +This is because Action Cable uses the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking) to take over control of connections from the application server. Action Cable then manages connections internally, in a multithreaded manner, regardless of whether the application server is multi-threaded or not. So Action Cable works diff --git a/actioncable/Rakefile b/actioncable/Rakefile index fb75d14363..52c86eb5d7 100644 --- a/actioncable/Rakefile +++ b/actioncable/Rakefile @@ -7,7 +7,7 @@ require "action_cable" task default: :test -task package: %w( assets:compile assets:verify ) +task :package Rake::TestTask.new do |t| t.libs << "test" @@ -35,42 +35,13 @@ namespace :test do end namespace :assets do - desc "Compile Action Cable assets" - task :compile do - require "blade" - require "sprockets" - require "sprockets/export" - Blade.build - end - - desc "Verify compiled Action Cable assets" - task :verify do - file = "lib/assets/compiled/action_cable.js" - pathname = Pathname.new("#{__dir__}/#{file}") - - print "[verify] #{file} exists " - if pathname.exist? - puts "[OK]" - else - $stderr.puts "[FAIL]" - fail - end + desc "Generate ActionCable::INTERNAL JS module" + task :codegen do + require "json" + require "action_cable" - print "[verify] #{file} is a UMD module " - if /module\.exports.*define\.amd/m.match?(pathname.read) - puts "[OK]" - else - $stderr.puts "[FAIL]" - fail - end - - print "[verify] #{__dir__} can be required as a module " - _, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{__dir__}');") - if status.success? - puts "[OK]" - else - $stderr.puts "[FAIL]\n#{stderr}" - fail + File.open(File.join(__dir__, "app/javascript/action_cable/internal.js").to_s, "w+") do |file| + file.write("export default #{JSON.generate(ActionCable::INTERNAL)}") end end end diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index d946d0797f..31c6fb41f2 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.email = ["pratiknaik@gmail.com", "david@loudthinking.com"] s.homepage = "http://rubyonrails.org" - s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*"] + s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"] s.require_path = "lib" s.metadata = { @@ -25,6 +25,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actioncable/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "actionpack", version s.add_dependency "nio4r", "~> 2.0" diff --git a/actioncable/app/assets/javascripts/action_cable.coffee.erb b/actioncable/app/assets/javascripts/action_cable.coffee.erb deleted file mode 100644 index e0758dae72..0000000000 --- a/actioncable/app/assets/javascripts/action_cable.coffee.erb +++ /dev/null @@ -1,38 +0,0 @@ -#= export ActionCable -#= require_self -#= require ./action_cable/consumer - -@ActionCable = - INTERNAL: <%= ActionCable::INTERNAL.to_json %> - WebSocket: window.WebSocket - logger: window.console - - createConsumer: (url) -> - url ?= @getConfig("url") ? @INTERNAL.default_mount_path - new ActionCable.Consumer @createWebSocketURL(url) - - getConfig: (name) -> - element = document.head.querySelector("meta[name='action-cable-#{name}']") - element?.getAttribute("content") - - createWebSocketURL: (url) -> - if url and not /^wss?:/i.test(url) - a = document.createElement("a") - a.href = url - # Fix populating Location properties in IE. Otherwise, protocol will be blank. - a.href = a.href - a.protocol = a.protocol.replace("http", "ws") - a.href - else - url - - startDebugging: -> - @debugging = true - - stopDebugging: -> - @debugging = null - - log: (messages...) -> - if @debugging - messages.push(Date.now()) - @logger.log("[ActionCable]", messages...) diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js new file mode 100644 index 0000000000..df60b1fc24 --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable.js @@ -0,0 +1,475 @@ +(function(global, factory) { + typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : global.ActionCable = factory(); +})(this, function() { + "use strict"; + var INTERNAL = { + message_types: { + welcome: "welcome", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" + }, + default_mount_path: "/cable", + protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] + }; + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { + return typeof obj; + } : function(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + var classCallCheck = function(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols; + var supportedProtocols = protocols.slice(0, protocols.length - 1); + var indexOf = [].indexOf; + var Connection = function() { + function Connection(consumer) { + classCallCheck(this, Connection); + this.open = this.open.bind(this); + this.consumer = consumer; + this.subscriptions = this.consumer.subscriptions; + this.monitor = new ActionCable.ConnectionMonitor(this); + this.disconnected = true; + } + Connection.prototype.send = function send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)); + return true; + } else { + return false; + } + }; + Connection.prototype.open = function open() { + if (this.isActive()) { + ActionCable.log("Attempted to open WebSocket, but existing socket is " + this.getState()); + return false; + } else { + ActionCable.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols); + if (this.webSocket) { + this.uninstallEventHandlers(); + } + this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols); + this.installEventHandlers(); + this.monitor.start(); + return true; + } + }; + Connection.prototype.close = function close() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { + allowReconnect: true + }, allowReconnect = _ref.allowReconnect; + if (!allowReconnect) { + this.monitor.stop(); + } + if (this.isActive()) { + return this.webSocket ? this.webSocket.close() : undefined; + } + }; + Connection.prototype.reopen = function reopen() { + ActionCable.log("Reopening WebSocket, current state is " + this.getState()); + if (this.isActive()) { + try { + return this.close(); + } catch (error) { + ActionCable.log("Failed to reopen WebSocket", error); + } finally { + ActionCable.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms"); + setTimeout(this.open, this.constructor.reopenDelay); + } + } else { + return this.open(); + } + }; + Connection.prototype.getProtocol = function getProtocol() { + return this.webSocket ? this.webSocket.protocol : undefined; + }; + Connection.prototype.isOpen = function isOpen() { + return this.isState("open"); + }; + Connection.prototype.isActive = function isActive() { + return this.isState("open", "connecting"); + }; + Connection.prototype.isProtocolSupported = function isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; + }; + Connection.prototype.isState = function isState() { + for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) { + states[_key] = arguments[_key]; + } + return indexOf.call(states, this.getState()) >= 0; + }; + Connection.prototype.getState = function getState() { + if (this.webSocket) { + for (var state in WebSocket) { + if (WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase(); + } + } + } + return null; + }; + Connection.prototype.installEventHandlers = function installEventHandlers() { + for (var eventName in this.events) { + var handler = this.events[eventName].bind(this); + this.webSocket["on" + eventName] = handler; + } + }; + Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() { + for (var eventName in this.events) { + this.webSocket["on" + eventName] = function() {}; + } + }; + return Connection; + }(); + Connection.reopenDelay = 500; + Connection.prototype.events = { + message: function message(event) { + if (!this.isProtocolSupported()) { + return; + } + var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, type = _JSON$parse.type; + switch (type) { + case message_types.welcome: + this.monitor.recordConnect(); + return this.subscriptions.reload(); + + case message_types.ping: + return this.monitor.recordPing(); + + case message_types.confirmation: + return this.subscriptions.notify(identifier, "connected"); + + case message_types.rejection: + return this.subscriptions.reject(identifier); + + default: + return this.subscriptions.notify(identifier, "received", message); + } + }, + open: function open() { + ActionCable.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol"); + this.disconnected = false; + if (!this.isProtocolSupported()) { + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting."); + return this.close({ + allowReconnect: false + }); + } + }, + close: function close(event) { + ActionCable.log("WebSocket onclose event"); + if (this.disconnected) { + return; + } + this.disconnected = true; + this.monitor.recordDisconnect(); + return this.subscriptions.notifyAll("disconnected", { + willAttemptReconnect: this.monitor.isRunning() + }); + }, + error: function error() { + ActionCable.log("WebSocket onerror event"); + } + }; + var now = function now() { + return new Date().getTime(); + }; + var secondsSince = function secondsSince(time) { + return (now() - time) / 1e3; + }; + var clamp = function clamp(number, min, max) { + return Math.max(min, Math.min(max, number)); + }; + var ConnectionMonitor = function() { + function ConnectionMonitor(connection) { + classCallCheck(this, ConnectionMonitor); + this.visibilityDidChange = this.visibilityDidChange.bind(this); + this.connection = connection; + this.reconnectAttempts = 0; + } + ConnectionMonitor.prototype.start = function start() { + if (!this.isRunning()) { + this.startedAt = now(); + delete this.stoppedAt; + this.startPolling(); + document.addEventListener("visibilitychange", this.visibilityDidChange); + ActionCable.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms"); + } + }; + ConnectionMonitor.prototype.stop = function stop() { + if (this.isRunning()) { + this.stoppedAt = now(); + this.stopPolling(); + document.removeEventListener("visibilitychange", this.visibilityDidChange); + ActionCable.log("ConnectionMonitor stopped"); + } + }; + ConnectionMonitor.prototype.isRunning = function isRunning() { + return this.startedAt && !this.stoppedAt; + }; + ConnectionMonitor.prototype.recordPing = function recordPing() { + this.pingedAt = now(); + }; + ConnectionMonitor.prototype.recordConnect = function recordConnect() { + this.reconnectAttempts = 0; + this.recordPing(); + delete this.disconnectedAt; + ActionCable.log("ConnectionMonitor recorded connect"); + }; + ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() { + this.disconnectedAt = now(); + ActionCable.log("ConnectionMonitor recorded disconnect"); + }; + ConnectionMonitor.prototype.startPolling = function startPolling() { + this.stopPolling(); + this.poll(); + }; + ConnectionMonitor.prototype.stopPolling = function stopPolling() { + clearTimeout(this.pollTimeout); + }; + ConnectionMonitor.prototype.poll = function poll() { + var _this = this; + this.pollTimeout = setTimeout(function() { + _this.reconnectIfStale(); + _this.poll(); + }, this.getPollInterval()); + }; + ConnectionMonitor.prototype.getPollInterval = function getPollInterval() { + var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max; + var interval = 5 * Math.log(this.reconnectAttempts + 1); + return Math.round(clamp(interval, min, max) * 1e3); + }; + ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { + if (this.connectionIsStale()) { + ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + this.getPollInterval() + " ms, time disconnected = " + secondsSince(this.disconnectedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s"); + this.reconnectAttempts++; + if (this.disconnectedRecently()) { + ActionCable.log("ConnectionMonitor skipping reopening recent disconnect"); + } else { + ActionCable.log("ConnectionMonitor reopening"); + this.connection.reopen(); + } + } + }; + ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() { + return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold; + }; + ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() { + return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; + }; + ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() { + var _this2 = this; + if (document.visibilityState === "visible") { + setTimeout(function() { + if (_this2.connectionIsStale() || !_this2.connection.isOpen()) { + ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState); + _this2.connection.reopen(); + } + }, 200); + } + }; + return ConnectionMonitor; + }(); + ConnectionMonitor.pollInterval = { + min: 3, + max: 30 + }; + ConnectionMonitor.staleThreshold = 6; + var Consumer = function() { + function Consumer(url) { + classCallCheck(this, Consumer); + this.url = url; + this.subscriptions = new ActionCable.Subscriptions(this); + this.connection = new ActionCable.Connection(this); + } + Consumer.prototype.send = function send(data) { + return this.connection.send(data); + }; + Consumer.prototype.connect = function connect() { + return this.connection.open(); + }; + Consumer.prototype.disconnect = function disconnect() { + return this.connection.close({ + allowReconnect: false + }); + }; + Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open(); + } + }; + return Consumer; + }(); + var extend = function extend(object, properties) { + if (properties != null) { + for (var key in properties) { + var value = properties[key]; + object[key] = value; + } + } + return object; + }; + var Subscription = function() { + function Subscription(consumer) { + var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var mixin = arguments[2]; + classCallCheck(this, Subscription); + this.consumer = consumer; + this.identifier = JSON.stringify(params); + extend(this, mixin); + } + Subscription.prototype.perform = function perform(action) { + var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + data.action = action; + return this.send(data); + }; + Subscription.prototype.send = function send(data) { + return this.consumer.send({ + command: "message", + identifier: this.identifier, + data: JSON.stringify(data) + }); + }; + Subscription.prototype.unsubscribe = function unsubscribe() { + return this.consumer.subscriptions.remove(this); + }; + return Subscription; + }(); + var Subscriptions = function() { + function Subscriptions(consumer) { + classCallCheck(this, Subscriptions); + this.consumer = consumer; + this.subscriptions = []; + } + Subscriptions.prototype.create = function create(channelName, mixin) { + var channel = channelName; + var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : { + channel: channel + }; + var subscription = new ActionCable.Subscription(this.consumer, params, mixin); + return this.add(subscription); + }; + Subscriptions.prototype.add = function add(subscription) { + this.subscriptions.push(subscription); + this.consumer.ensureActiveConnection(); + this.notify(subscription, "initialized"); + this.sendCommand(subscription, "subscribe"); + return subscription; + }; + Subscriptions.prototype.remove = function remove(subscription) { + this.forget(subscription); + if (!this.findAll(subscription.identifier).length) { + this.sendCommand(subscription, "unsubscribe"); + } + return subscription; + }; + Subscriptions.prototype.reject = function reject(identifier) { + var _this = this; + return this.findAll(identifier).map(function(subscription) { + _this.forget(subscription); + _this.notify(subscription, "rejected"); + return subscription; + }); + }; + Subscriptions.prototype.forget = function forget(subscription) { + this.subscriptions = this.subscriptions.filter(function(s) { + return s !== subscription; + }); + return subscription; + }; + Subscriptions.prototype.findAll = function findAll(identifier) { + return this.subscriptions.filter(function(s) { + return s.identifier === identifier; + }); + }; + Subscriptions.prototype.reload = function reload() { + var _this2 = this; + return this.subscriptions.map(function(subscription) { + return _this2.sendCommand(subscription, "subscribe"); + }); + }; + Subscriptions.prototype.notifyAll = function notifyAll(callbackName) { + var _this3 = this; + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return this.subscriptions.map(function(subscription) { + return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args)); + }); + }; + Subscriptions.prototype.notify = function notify(subscription, callbackName) { + for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { + args[_key2 - 2] = arguments[_key2]; + } + var subscriptions = void 0; + if (typeof subscription === "string") { + subscriptions = this.findAll(subscription); + } else { + subscriptions = [ subscription ]; + } + return subscriptions.map(function(subscription) { + return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined; + }); + }; + Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) { + var identifier = subscription.identifier; + return this.consumer.send({ + command: command, + identifier: identifier + }); + }; + return Subscriptions; + }(); + var ActionCable = { + Connection: Connection, + ConnectionMonitor: ConnectionMonitor, + Consumer: Consumer, + INTERNAL: INTERNAL, + Subscription: Subscription, + Subscriptions: Subscriptions, + WebSocket: window.WebSocket, + logger: window.console, + createConsumer: function createConsumer(url) { + if (url == null) { + var urlConfig = this.getConfig("url"); + url = urlConfig ? urlConfig : this.INTERNAL.default_mount_path; + } + return new Consumer(this.createWebSocketURL(url)); + }, + getConfig: function getConfig(name) { + var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); + return element ? element.getAttribute("content") : undefined; + }, + createWebSocketURL: function createWebSocketURL(url) { + if (url && !/^wss?:/i.test(url)) { + var a = document.createElement("a"); + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace("http", "ws"); + return a.href; + } else { + return url; + } + }, + startDebugging: function startDebugging() { + this.debugging = true; + }, + stopDebugging: function stopDebugging() { + this.debugging = null; + }, + log: function log() { + if (this.debugging) { + var _logger; + for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) { + messages[_key] = arguments[_key]; + } + messages.push(Date.now()); + (_logger = this.logger).log.apply(_logger, [ "[ActionCable]" ].concat(messages)); + } + } + }; + return ActionCable; +}); diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee deleted file mode 100644 index 7fd68cad2f..0000000000 --- a/actioncable/app/assets/javascripts/action_cable/connection.coffee +++ /dev/null @@ -1,116 +0,0 @@ -#= require ./connection_monitor - -# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. - -{message_types, protocols} = ActionCable.INTERNAL -[supportedProtocols..., unsupportedProtocol] = protocols - -class ActionCable.Connection - @reopenDelay: 500 - - constructor: (@consumer) -> - {@subscriptions} = @consumer - @monitor = new ActionCable.ConnectionMonitor this - @disconnected = true - - send: (data) -> - if @isOpen() - @webSocket.send(JSON.stringify(data)) - true - else - false - - open: => - if @isActive() - ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}") - false - else - ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") - @uninstallEventHandlers() if @webSocket? - @webSocket = new ActionCable.WebSocket(@consumer.url, protocols) - @installEventHandlers() - @monitor.start() - true - - close: ({allowReconnect} = {allowReconnect: true}) -> - @monitor.stop() unless allowReconnect - @webSocket?.close() if @isActive() - - reopen: -> - ActionCable.log("Reopening WebSocket, current state is #{@getState()}") - if @isActive() - try - @close() - catch error - ActionCable.log("Failed to reopen WebSocket", error) - finally - ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms") - setTimeout(@open, @constructor.reopenDelay) - else - @open() - - getProtocol: -> - @webSocket?.protocol - - isOpen: -> - @isState("open") - - isActive: -> - @isState("open", "connecting") - - # Private - - isProtocolSupported: -> - @getProtocol() in supportedProtocols - - isState: (states...) -> - @getState() in states - - getState: -> - return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState - null - - installEventHandlers: -> - for eventName of @events - handler = @events[eventName].bind(this) - @webSocket["on#{eventName}"] = handler - return - - uninstallEventHandlers: -> - for eventName of @events - @webSocket["on#{eventName}"] = -> - return - - events: - message: (event) -> - return unless @isProtocolSupported() - {identifier, message, type} = JSON.parse(event.data) - switch type - when message_types.welcome - @monitor.recordConnect() - @subscriptions.reload() - when message_types.ping - @monitor.recordPing() - when message_types.confirmation - @subscriptions.notify(identifier, "connected") - when message_types.rejection - @subscriptions.reject(identifier) - else - @subscriptions.notify(identifier, "received", message) - - open: -> - ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol") - @disconnected = false - if not @isProtocolSupported() - ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") - @close(allowReconnect: false) - - close: (event) -> - ActionCable.log("WebSocket onclose event") - return if @disconnected - @disconnected = true - @monitor.recordDisconnect() - @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()}) - - error: -> - ActionCable.log("WebSocket onerror event") diff --git a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee deleted file mode 100644 index 0cc675fa94..0000000000 --- a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee +++ /dev/null @@ -1,95 +0,0 @@ -# Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting -# revival reconnections if things go astray. Internal class, not intended for direct user manipulation. -class ActionCable.ConnectionMonitor - @pollInterval: - min: 3 - max: 30 - - @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings) - - constructor: (@connection) -> - @reconnectAttempts = 0 - - start: -> - unless @isRunning() - @startedAt = now() - delete @stoppedAt - @startPolling() - document.addEventListener("visibilitychange", @visibilityDidChange) - ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms") - - stop: -> - if @isRunning() - @stoppedAt = now() - @stopPolling() - document.removeEventListener("visibilitychange", @visibilityDidChange) - ActionCable.log("ConnectionMonitor stopped") - - isRunning: -> - @startedAt? and not @stoppedAt? - - recordPing: -> - @pingedAt = now() - - recordConnect: -> - @reconnectAttempts = 0 - @recordPing() - delete @disconnectedAt - ActionCable.log("ConnectionMonitor recorded connect") - - recordDisconnect: -> - @disconnectedAt = now() - ActionCable.log("ConnectionMonitor recorded disconnect") - - # Private - - startPolling: -> - @stopPolling() - @poll() - - stopPolling: -> - clearTimeout(@pollTimeout) - - poll: -> - @pollTimeout = setTimeout => - @reconnectIfStale() - @poll() - , @getPollInterval() - - getPollInterval: -> - {min, max} = @constructor.pollInterval - interval = 5 * Math.log(@reconnectAttempts + 1) - Math.round(clamp(interval, min, max) * 1000) - - reconnectIfStale: -> - if @connectionIsStale() - ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s") - @reconnectAttempts++ - if @disconnectedRecently() - ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") - else - ActionCable.log("ConnectionMonitor reopening") - @connection.reopen() - - connectionIsStale: -> - secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold - - disconnectedRecently: -> - @disconnectedAt and secondsSince(@disconnectedAt) < @constructor.staleThreshold - - visibilityDidChange: => - if document.visibilityState is "visible" - setTimeout => - if @connectionIsStale() or not @connection.isOpen() - ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = #{document.visibilityState}") - @connection.reopen() - , 200 - - now = -> - new Date().getTime() - - secondsSince = (time) -> - (now() - time) / 1000 - - clamp = (number, min, max) -> - Math.max(min, Math.min(max, number)) diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee deleted file mode 100644 index 3298be717f..0000000000 --- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee +++ /dev/null @@ -1,46 +0,0 @@ -#= require ./connection -#= require ./subscriptions -#= require ./subscription - -# The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established, -# the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates. -# The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription -# method. -# -# The following example shows how this can be setup: -# -# @App = {} -# App.cable = ActionCable.createConsumer "ws://example.com/accounts/1" -# App.appearance = App.cable.subscriptions.create "AppearanceChannel" -# -# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. -# -# When a consumer is created, it automatically connects with the server. -# -# To disconnect from the server, call -# -# App.cable.disconnect() -# -# and to restart the connection: -# -# App.cable.connect() -# -# Any channel subscriptions which existed prior to disconnecting will -# automatically resubscribe. -class ActionCable.Consumer - constructor: (@url) -> - @subscriptions = new ActionCable.Subscriptions this - @connection = new ActionCable.Connection this - - send: (data) -> - @connection.send(data) - - connect: -> - @connection.open() - - disconnect: -> - @connection.close(allowReconnect: false) - - ensureActiveConnection: -> - unless @connection.isActive() - @connection.open() diff --git a/actioncable/app/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee deleted file mode 100644 index 8e0805a174..0000000000 --- a/actioncable/app/assets/javascripts/action_cable/subscription.coffee +++ /dev/null @@ -1,72 +0,0 @@ -# A new subscription is created through the ActionCable.Subscriptions instance available on the consumer. -# It provides a number of callbacks and a method for calling remote procedure calls on the corresponding -# Channel instance on the server side. -# -# An example demonstrates the basic functionality: -# -# App.appearance = App.cable.subscriptions.create "AppearanceChannel", -# connected: -> -# # Called once the subscription has been successfully completed -# -# disconnected: ({ willAttemptReconnect: boolean }) -> -# # Called when the client has disconnected with the server. -# # The object will have an `willAttemptReconnect` property which -# # says whether the client has the intention of attempting -# # to reconnect. -# -# appear: -> -# @perform 'appear', appearing_on: @appearingOn() -# -# away: -> -# @perform 'away' -# -# appearingOn: -> -# $('main').data 'appearing-on' -# -# The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server -# by calling the `@perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away). -# The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter. -# -# This is how the server component would look: -# -# class AppearanceChannel < ApplicationActionCable::Channel -# def subscribed -# current_user.appear -# end -# -# def unsubscribed -# current_user.disappear -# end -# -# def appear(data) -# current_user.appear on: data['appearing_on'] -# end -# -# def away -# current_user.away -# end -# end -# -# The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name. -# The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the @perform method. -class ActionCable.Subscription - constructor: (@consumer, params = {}, mixin) -> - @identifier = JSON.stringify(params) - extend(this, mixin) - - # Perform a channel action with the optional data passed as an attribute - perform: (action, data = {}) -> - data.action = action - @send(data) - - send: (data) -> - @consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data)) - - unsubscribe: -> - @consumer.subscriptions.remove(this) - - extend = (object, properties) -> - if properties? - for key, value of properties - object[key] = value - object diff --git a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee deleted file mode 100644 index aa052bf5d8..0000000000 --- a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee +++ /dev/null @@ -1,66 +0,0 @@ -# Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user -# us ActionCable.Subscriptions#create, and it should be called through the consumer like so: -# -# @App = {} -# App.cable = ActionCable.createConsumer "ws://example.com/accounts/1" -# App.appearance = App.cable.subscriptions.create "AppearanceChannel" -# -# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. -class ActionCable.Subscriptions - constructor: (@consumer) -> - @subscriptions = [] - - create: (channelName, mixin) -> - channel = channelName - params = if typeof channel is "object" then channel else {channel} - subscription = new ActionCable.Subscription @consumer, params, mixin - @add(subscription) - - # Private - - add: (subscription) -> - @subscriptions.push(subscription) - @consumer.ensureActiveConnection() - @notify(subscription, "initialized") - @sendCommand(subscription, "subscribe") - subscription - - remove: (subscription) -> - @forget(subscription) - unless @findAll(subscription.identifier).length - @sendCommand(subscription, "unsubscribe") - subscription - - reject: (identifier) -> - for subscription in @findAll(identifier) - @forget(subscription) - @notify(subscription, "rejected") - subscription - - forget: (subscription) -> - @subscriptions = (s for s in @subscriptions when s isnt subscription) - subscription - - findAll: (identifier) -> - s for s in @subscriptions when s.identifier is identifier - - reload: -> - for subscription in @subscriptions - @sendCommand(subscription, "subscribe") - - notifyAll: (callbackName, args...) -> - for subscription in @subscriptions - @notify(subscription, callbackName, args...) - - notify: (subscription, callbackName, args...) -> - if typeof subscription is "string" - subscriptions = @findAll(subscription) - else - subscriptions = [subscription] - - for subscription in subscriptions - subscription[callbackName]?(args...) - - sendCommand: (subscription, command) -> - {identifier} = subscription - @consumer.send({command, identifier}) diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js new file mode 100644 index 0000000000..4ad436f2c0 --- /dev/null +++ b/actioncable/app/javascript/action_cable/connection.js @@ -0,0 +1,156 @@ +import ActionCable from "./index" +import INTERNAL from "./internal" + +// Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. + +const {message_types, protocols} = INTERNAL +const supportedProtocols = protocols.slice(0, protocols.length - 1) + +const indexOf = [].indexOf + +class Connection { + constructor(consumer) { + this.open = this.open.bind(this) + this.consumer = consumer + this.subscriptions = this.consumer.subscriptions + this.monitor = new ActionCable.ConnectionMonitor(this) + this.disconnected = true + } + + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)) + return true + } else { + return false + } + } + + open() { + if (this.isActive()) { + ActionCable.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) + return false + } else { + ActionCable.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) + if (this.webSocket) { this.uninstallEventHandlers() } + this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) + this.installEventHandlers() + this.monitor.start() + return true + } + } + + close({allowReconnect} = {allowReconnect: true}) { + if (!allowReconnect) { this.monitor.stop() } + if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) } + } + + reopen() { + ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) + if (this.isActive()) { + try { + return this.close() + } catch (error) { + ActionCable.log("Failed to reopen WebSocket", error) + } + finally { + ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) + setTimeout(this.open, this.constructor.reopenDelay) + } + } else { + return this.open() + } + } + + getProtocol() { + return (this.webSocket ? this.webSocket.protocol : undefined) + } + + isOpen() { + return this.isState("open") + } + + isActive() { + return this.isState("open", "connecting") + } + + // Private + + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0 + } + + isState(...states) { + return indexOf.call(states, this.getState()) >= 0 + } + + getState() { + if (this.webSocket) { + for (let state in WebSocket) { + if (WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase() + } + } + } + return null + } + + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this) + this.webSocket[`on${eventName}`] = handler + } + } + + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {} + } + } + +} + +Connection.reopenDelay = 500 + +Connection.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { return } + const {identifier, message, type} = JSON.parse(event.data) + switch (type) { + case message_types.welcome: + this.monitor.recordConnect() + return this.subscriptions.reload() + case message_types.ping: + return this.monitor.recordPing() + case message_types.confirmation: + return this.subscriptions.notify(identifier, "connected") + case message_types.rejection: + return this.subscriptions.reject(identifier) + default: + return this.subscriptions.notify(identifier, "received", message) + } + }, + + open() { + ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) + this.disconnected = false + if (!this.isProtocolSupported()) { + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + return this.close({allowReconnect: false}) + } + }, + + close(event) { + ActionCable.log("WebSocket onclose event") + if (this.disconnected) { return } + this.disconnected = true + this.monitor.recordDisconnect() + return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()}) + }, + + error() { + ActionCable.log("WebSocket onerror event") + } +} + +export default Connection diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js new file mode 100644 index 0000000000..8d6ac1f682 --- /dev/null +++ b/actioncable/app/javascript/action_cable/connection_monitor.js @@ -0,0 +1,125 @@ +import ActionCable from "./index" + +// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting +// revival reconnections if things go astray. Internal class, not intended for direct user manipulation. + +const now = () => new Date().getTime() + +const secondsSince = time => (now() - time) / 1000 + +const clamp = (number, min, max) => Math.max(min, Math.min(max, number)) + +class ConnectionMonitor { + constructor(connection) { + this.visibilityDidChange = this.visibilityDidChange.bind(this) + this.connection = connection + this.reconnectAttempts = 0 + } + + start() { + if (!this.isRunning()) { + this.startedAt = now() + delete this.stoppedAt + this.startPolling() + document.addEventListener("visibilitychange", this.visibilityDidChange) + ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) + } + } + + stop() { + if (this.isRunning()) { + this.stoppedAt = now() + this.stopPolling() + document.removeEventListener("visibilitychange", this.visibilityDidChange) + ActionCable.log("ConnectionMonitor stopped") + } + } + + isRunning() { + return this.startedAt && !this.stoppedAt + } + + recordPing() { + this.pingedAt = now() + } + + recordConnect() { + this.reconnectAttempts = 0 + this.recordPing() + delete this.disconnectedAt + ActionCable.log("ConnectionMonitor recorded connect") + } + + recordDisconnect() { + this.disconnectedAt = now() + ActionCable.log("ConnectionMonitor recorded disconnect") + } + + // Private + + startPolling() { + this.stopPolling() + this.poll() + } + + stopPolling() { + clearTimeout(this.pollTimeout) + } + + poll() { + this.pollTimeout = setTimeout(() => { + this.reconnectIfStale() + this.poll() + } + , this.getPollInterval()) + } + + getPollInterval() { + const {min, max} = this.constructor.pollInterval + const interval = 5 * Math.log(this.reconnectAttempts + 1) + return Math.round(clamp(interval, min, max) * 1000) + } + + reconnectIfStale() { + if (this.connectionIsStale()) { + ActionCable.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`) + this.reconnectAttempts++ + if (this.disconnectedRecently()) { + ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") + } else { + ActionCable.log("ConnectionMonitor reopening") + this.connection.reopen() + } + } + } + + connectionIsStale() { + return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold + } + + disconnectedRecently() { + return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.constructor.staleThreshold) + } + + visibilityDidChange() { + if (document.visibilityState === "visible") { + setTimeout(() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`) + this.connection.reopen() + } + } + , 200) + } + } + +} + +ConnectionMonitor.pollInterval = { + min: 3, + max: 30 +} + +ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings) + +export default ConnectionMonitor diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js new file mode 100644 index 0000000000..c484ceebbd --- /dev/null +++ b/actioncable/app/javascript/action_cable/consumer.js @@ -0,0 +1,53 @@ +import ActionCable from "./index" + +// The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established, +// the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates. +// The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription +// method. +// +// The following example shows how this can be setup: +// +// App = {} +// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1") +// App.appearance = App.cable.subscriptions.create("AppearanceChannel") +// +// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. +// +// When a consumer is created, it automatically connects with the server. +// +// To disconnect from the server, call +// +// App.cable.disconnect() +// +// and to restart the connection: +// +// App.cable.connect() +// +// Any channel subscriptions which existed prior to disconnecting will +// automatically resubscribe. + +export default class Consumer { + constructor(url) { + this.url = url + this.subscriptions = new ActionCable.Subscriptions(this) + this.connection = new ActionCable.Connection(this) + } + + send(data) { + return this.connection.send(data) + } + + connect() { + return this.connection.open() + } + + disconnect() { + return this.connection.close({allowReconnect: false}) + } + + ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open() + } + } +} diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js new file mode 100644 index 0000000000..eb0c4844df --- /dev/null +++ b/actioncable/app/javascript/action_cable/index.js @@ -0,0 +1,58 @@ +import Connection from "./connection" +import ConnectionMonitor from "./connection_monitor" +import Consumer from "./consumer" +import INTERNAL from "./internal" +import Subscription from "./subscription" +import Subscriptions from "./subscriptions" + +export default { + Connection, + ConnectionMonitor, + Consumer, + INTERNAL, + Subscription, + Subscriptions, + WebSocket: window.WebSocket, + logger: window.console, + + createConsumer(url) { + if (url == null) { + const urlConfig = this.getConfig("url") + url = (urlConfig ? urlConfig : this.INTERNAL.default_mount_path) + } + return new Consumer(this.createWebSocketURL(url)) + }, + + getConfig(name) { + const element = document.head.querySelector(`meta[name='action-cable-${name}']`) + return (element ? element.getAttribute("content") : undefined) + }, + + createWebSocketURL(url) { + if (url && !/^wss?:/i.test(url)) { + const a = document.createElement("a") + a.href = url + // Fix populating Location properties in IE. Otherwise, protocol will be blank. + a.href = a.href + a.protocol = a.protocol.replace("http", "ws") + return a.href + } else { + return url + } + }, + + startDebugging() { + this.debugging = true + }, + + stopDebugging() { + this.debugging = null + }, + + log(...messages) { + if (this.debugging) { + messages.push(Date.now()) + this.logger.log("[ActionCable]", ...messages) + } + } +} diff --git a/actioncable/app/javascript/action_cable/subscription.js b/actioncable/app/javascript/action_cable/subscription.js new file mode 100644 index 0000000000..7de08f93b3 --- /dev/null +++ b/actioncable/app/javascript/action_cable/subscription.js @@ -0,0 +1,89 @@ +// A new subscription is created through the ActionCable.Subscriptions instance available on the consumer. +// It provides a number of callbacks and a method for calling remote procedure calls on the corresponding +// Channel instance on the server side. +// +// An example demonstrates the basic functionality: +// +// App.appearance = App.cable.subscriptions.create("AppearanceChannel", { +// connected() { +// // Called once the subscription has been successfully completed +// }, +// +// disconnected({ willAttemptReconnect: boolean }) { +// // Called when the client has disconnected with the server. +// // The object will have an `willAttemptReconnect` property which +// // says whether the client has the intention of attempting +// // to reconnect. +// }, +// +// appear() { +// this.perform('appear', {appearing_on: this.appearingOn()}) +// }, +// +// away() { +// this.perform('away') +// }, +// +// appearingOn() { +// $('main').data('appearing-on') +// } +// }) +// +// The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server +// by calling the `perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away). +// The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter. +// +// This is how the server component would look: +// +// class AppearanceChannel < ApplicationActionCable::Channel +// def subscribed +// current_user.appear +// end +// +// def unsubscribed +// current_user.disappear +// end +// +// def appear(data) +// current_user.appear on: data['appearing_on'] +// end +// +// def away +// current_user.away +// end +// end +// +// The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name. +// The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the perform method. + +const extend = function(object, properties) { + if (properties != null) { + for (let key in properties) { + const value = properties[key] + object[key] = value + } + } + return object +} + +export default class Subscription { + constructor(consumer, params = {}, mixin) { + this.consumer = consumer + this.identifier = JSON.stringify(params) + extend(this, mixin) + } + + // Perform a channel action with the optional data passed as an attribute + perform(action, data = {}) { + data.action = action + return this.send(data) + } + + send(data) { + return this.consumer.send({command: "message", identifier: this.identifier, data: JSON.stringify(data)}) + } + + unsubscribe() { + return this.consumer.subscriptions.remove(this) + } +} diff --git a/actioncable/app/javascript/action_cable/subscriptions.js b/actioncable/app/javascript/action_cable/subscriptions.js new file mode 100644 index 0000000000..712ff50d28 --- /dev/null +++ b/actioncable/app/javascript/action_cable/subscriptions.js @@ -0,0 +1,86 @@ +import ActionCable from "./index" + +// Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user +// us ActionCable.Subscriptions#create, and it should be called through the consumer like so: +// +// App = {} +// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1") +// App.appearance = App.cable.subscriptions.create("AppearanceChannel") +// +// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. + +export default class Subscriptions { + constructor(consumer) { + this.consumer = consumer + this.subscriptions = [] + } + + create(channelName, mixin) { + const channel = channelName + const params = typeof channel === "object" ? channel : {channel} + const subscription = new ActionCable.Subscription(this.consumer, params, mixin) + return this.add(subscription) + } + + // Private + + add(subscription) { + this.subscriptions.push(subscription) + this.consumer.ensureActiveConnection() + this.notify(subscription, "initialized") + this.sendCommand(subscription, "subscribe") + return subscription + } + + remove(subscription) { + this.forget(subscription) + if (!this.findAll(subscription.identifier).length) { + this.sendCommand(subscription, "unsubscribe") + } + return subscription + } + + reject(identifier) { + return this.findAll(identifier).map((subscription) => { + this.forget(subscription) + this.notify(subscription, "rejected") + return subscription + }) + } + + forget(subscription) { + this.subscriptions = (this.subscriptions.filter((s) => s !== subscription)) + return subscription + } + + findAll(identifier) { + return this.subscriptions.filter((s) => s.identifier === identifier) + } + + reload() { + return this.subscriptions.map((subscription) => + this.sendCommand(subscription, "subscribe")) + } + + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription) => + this.notify(subscription, callbackName, ...args)) + } + + notify(subscription, callbackName, ...args) { + let subscriptions + if (typeof subscription === "string") { + subscriptions = this.findAll(subscription) + } else { + subscriptions = [subscription] + } + + return subscriptions.map((subscription) => + (typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)) + } + + sendCommand(subscription, command) { + const {identifier} = subscription + return this.consumer.send({command, identifier}) + } +} diff --git a/actioncable/blade.yml b/actioncable/blade.yml index e38e9b2f1d..54d13a2ba2 100644 --- a/actioncable/blade.yml +++ b/actioncable/blade.yml @@ -6,12 +6,6 @@ load_paths: logical_paths: - test.js -build: - logical_paths: - - action_cable.js - path: lib/assets/compiled - clean: true - plugins: sauce_labs: browsers: diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index e7456e3c1b..d261d4112e 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -32,13 +32,13 @@ module ActionCable INTERNAL = { message_types: { - welcome: "welcome".freeze, - ping: "ping".freeze, - confirmation: "confirm_subscription".freeze, - rejection: "reject_subscription".freeze + welcome: "welcome", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" }, - default_mount_path: "/cable".freeze, - protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze + default_mount_path: "/cable", + protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze } # Singleton instance of the server @@ -51,4 +51,6 @@ module ActionCable autoload :Channel autoload :RemoteConnections autoload :SubscriptionAdapter + autoload :TestHelper + autoload :TestCase end diff --git a/actioncable/lib/action_cable/channel.rb b/actioncable/lib/action_cable/channel.rb index d2f6fbbbc7..d5118b9dc9 100644 --- a/actioncable/lib/action_cable/channel.rb +++ b/actioncable/lib/action_cable/channel.rb @@ -11,6 +11,7 @@ module ActionCable autoload :Naming autoload :PeriodicTimers autoload :Streams + autoload :TestCase end end end diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index c5ad749bfe..70c93ec0f3 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -270,7 +270,7 @@ module ActionCable end def action_signature(action, data) - "#{self.class.name}##{action}".dup.tap do |signature| + (+"#{self.class.name}##{action}").tap do |signature| if (arguments = data.except("action")).any? signature << "(#{arguments.inspect})" end diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb new file mode 100644 index 0000000000..dd2762ccd0 --- /dev/null +++ b/actioncable/lib/action_cable/channel/test_case.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require "active_support" +require "active_support/test_case" +require "active_support/core_ext/hash/indifferent_access" +require "json" + +module ActionCable + module Channel + class NonInferrableChannelError < ::StandardError + def initialize(name) + super "Unable to determine the channel to test from #{name}. " + + "You'll need to specify it using `tests YourChannel` in your " + + "test case definition." + end + end + + # Stub `stream_from` to track streams for the channel. + # Add public aliases for `subscription_confirmation_sent?` and + # `subscription_rejected?`. + module ChannelStub + def confirmed? + subscription_confirmation_sent? + end + + def rejected? + subscription_rejected? + end + + def stream_from(broadcasting, *) + streams << broadcasting + end + + def stop_all_streams + @_streams = [] + end + + def streams + @_streams ||= [] + end + + # Make periodic timers no-op + def start_periodic_timers; end + alias stop_periodic_timers start_periodic_timers + end + + class ConnectionStub + attr_reader :transmissions, :identifiers, :subscriptions, :logger + + def initialize(identifiers = {}) + @transmissions = [] + + identifiers.each do |identifier, val| + define_singleton_method(identifier) { val } + end + + @subscriptions = ActionCable::Connection::Subscriptions.new(self) + @identifiers = identifiers.keys + @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) + end + + def transmit(cable_message) + transmissions << cable_message.with_indifferent_access + end + end + + # Superclass for Action Cable channel functional tests. + # + # == Basic example + # + # Functional tests are written as follows: + # 1. First, one uses the +subscribe+ method to simulate subscription creation. + # 2. Then, one asserts whether the current state is as expected. "State" can be anything: + # transmitted messages, subscribed streams, etc. + # + # For example: + # + # class ChatChannelTest < ActionCable::Channel::TestCase + # def test_subscribed_with_room_number + # # Simulate a subscription creation + # subscribe room_number: 1 + # + # # Asserts that the subscription was successfully created + # assert subscription.confirmed? + # + # # Asserts that the channel subscribes connection to a stream + # assert_equal "chat_1", streams.last + # end + # + # def test_does_not_subscribe_without_room_number + # subscribe + # + # # Asserts that the subscription was rejected + # assert subscription.rejected? + # end + # end + # + # You can also perform actions: + # def test_perform_speak + # subscribe room_number: 1 + # + # perform :speak, message: "Hello, Rails!" + # + # assert_equal "Hello, Rails!", transmissions.last["text"] + # end + # + # == Special methods + # + # ActionCable::Channel::TestCase will also automatically provide the following instance + # methods for use in the tests: + # + # <b>connection</b>:: + # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection. + # <b>subscription</b>:: + # An instance of the current channel, created when you call `subscribe`. + # <b>transmissions</b>:: + # A list of all messages that have been transmitted into the channel. + # <b>streams</b>:: + # A list of all created streams subscriptions (as identifiers) for the subscription. + # + # + # == Channel is automatically inferred + # + # ActionCable::Channel::TestCase will automatically infer the channel under test + # from the test class name. If the channel cannot be inferred from the test + # class name, you can explicitly set it with +tests+. + # + # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase + # tests SpecialChannel + # end + # + # == Specifying connection identifiers + # + # You need to set up your connection manually to provide values for the identifiers. + # To do this just use: + # + # stub_connection(user: users[:john]) + # + # == Testing broadcasting + # + # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g. + # +assert_broadcasts+) to handle broadcasting to models: + # + # + # # in your channel + # def speak(data) + # broadcast_to room, text: data["message"] + # end + # + # def test_speak + # subscribe room_id: rooms[:chat].id + # + # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do + # perform :speak, message: "Hello, Rails!" + # end + # end + class TestCase < ActiveSupport::TestCase + module Behavior + extend ActiveSupport::Concern + + include ActiveSupport::Testing::ConstantLookup + include ActionCable::TestHelper + + CHANNEL_IDENTIFIER = "test_stub" + + included do + class_attribute :_channel_class + + attr_reader :connection, :subscription + delegate :streams, to: :subscription + + ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self) + end + + module ClassMethods + def tests(channel) + case channel + when String, Symbol + self._channel_class = channel.to_s.camelize.constantize + when Module + self._channel_class = channel + else + raise NonInferrableChannelError.new(channel) + end + end + + def channel_class + if channel = self._channel_class + channel + else + tests determine_default_channel(name) + end + end + + def determine_default_channel(name) + channel = determine_constant_from_test_name(name) do |constant| + Class === constant && constant < ActionCable::Channel::Base + end + raise NonInferrableChannelError.new(name) if channel.nil? + channel + end + end + + # Setup test connection with the specified identifiers: + # + # class ApplicationCable < ActionCable::Connection::Base + # identified_by :user, :token + # end + # + # stub_connection(user: users[:john], token: 'my-secret-token') + def stub_connection(identifiers = {}) + @connection = ConnectionStub.new(identifiers) + end + + # Subscribe to the channel under test. Optionally pass subscription parameters as a Hash. + def subscribe(params = {}) + @connection ||= stub_connection + @subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access) + @subscription.singleton_class.include(ChannelStub) + @subscription.subscribe_to_channel + @subscription + end + + # Unsubscribe the subscription under test. + def unsubscribe + check_subscribed! + subscription.unsubscribe_from_channel + end + + # Perform action on a channel. + # + # NOTE: Must be subscribed. + def perform(action, data = {}) + check_subscribed! + subscription.perform_action(data.stringify_keys.merge("action" => action.to_s)) + end + + # Returns messages transmitted into channel + def transmissions + # Return only directly sent message (via #transmit) + connection.transmissions.map { |data| data["message"] }.compact + end + + # Enhance TestHelper assertions to handle non-String + # broadcastings + def assert_broadcasts(stream_or_object, *args) + super(broadcasting_for(stream_or_object), *args) + end + + def assert_broadcast_on(stream_or_object, *args) + super(broadcasting_for(stream_or_object), *args) + end + + private + def check_subscribed! + raise "Must be subscribed!" if subscription.nil? || subscription.rejected? + end + + def broadcasting_for(stream_or_object) + return stream_or_object if stream_or_object.is_a?(String) + + self.class.channel_class.broadcasting_for( + [self.class.channel_class.channel_name, stream_or_object] + ) + end + end + + include Behavior + end + end +end diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb index a22179d988..aef3386f71 100644 --- a/actioncable/lib/action_cable/connection/authorization.rb +++ b/actioncable/lib/action_cable/connection/authorization.rb @@ -5,7 +5,7 @@ module ActionCable module Authorization class UnauthorizedError < StandardError; end - # Closes the \WebSocket connection if it is open and returns a 404 "File not Found" response. + # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response. def reject_unauthorized_connection logger.error "An unauthorized connection attempt was rejected" raise UnauthorizedError diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb index bcece8d33b..6a9d5c2080 100644 --- a/actioncable/lib/action_cable/subscription_adapter.rb +++ b/actioncable/lib/action_cable/subscription_adapter.rb @@ -5,6 +5,7 @@ module ActionCable extend ActiveSupport::Autoload autoload :Base + autoload :Test autoload :SubscriberMap autoload :ChannelPrefix end diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index c28951608f..ad8fa52760 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -13,7 +13,8 @@ module ActionCable # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem. # This is needed, for example, when using Makara proxies for distributed Redis. cattr_accessor :redis_connector, default: ->(config) do - ::Redis.new(config.slice(:url, :host, :port, :db, :password)) + config[:id] ||= "ActionCable-PID-#{$$}" + ::Redis.new(config.slice(:url, :host, :port, :db, :password, :id)) end def initialize(*) diff --git a/actioncable/lib/action_cable/subscription_adapter/test.rb b/actioncable/lib/action_cable/subscription_adapter/test.rb new file mode 100644 index 0000000000..ce604cc88e --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "async" + +module ActionCable + module SubscriptionAdapter + # == Test adapter for Action Cable + # + # The test adapter should be used only in testing. Along with + # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application. + # + # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file. + # + # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter, + # so it could be used in system tests too. + class Test < Async + def broadcast(channel, payload) + broadcasts(channel) << payload + super + end + + def broadcasts(channel) + channels_data[channel] ||= [] + end + + def clear_messages(channel) + channels_data[channel] = [] + end + + def clear + @channels_data = nil + end + + private + def channels_data + @channels_data ||= {} + end + end + end +end diff --git a/actioncable/lib/action_cable/test_case.rb b/actioncable/lib/action_cable/test_case.rb new file mode 100644 index 0000000000..d153259bf6 --- /dev/null +++ b/actioncable/lib/action_cable/test_case.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "active_support/test_case" + +module ActionCable + class TestCase < ActiveSupport::TestCase + include ActionCable::TestHelper + + ActiveSupport.run_load_hooks(:action_cable_test_case, self) + end +end diff --git a/actioncable/lib/action_cable/test_helper.rb b/actioncable/lib/action_cable/test_helper.rb new file mode 100644 index 0000000000..7bc877663c --- /dev/null +++ b/actioncable/lib/action_cable/test_helper.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module ActionCable + # Provides helper methods for testing Action Cable broadcasting + module TestHelper + def before_setup # :nodoc: + server = ActionCable.server + test_adapter = ActionCable::SubscriptionAdapter::Test.new(server) + + @old_pubsub_adapter = server.pubsub + + server.instance_variable_set(:@pubsub, test_adapter) + super + end + + def after_teardown # :nodoc: + super + ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter) + end + + # Asserts that the number of broadcasted messages to the stream matches the given number. + # + # def test_broadcasts + # assert_broadcasts 'messages', 0 + # ActionCable.server.broadcast 'messages', { text: 'hello' } + # assert_broadcasts 'messages', 1 + # ActionCable.server.broadcast 'messages', { text: 'world' } + # assert_broadcasts 'messages', 2 + # end + # + # If a block is passed, that block should cause the specified number of + # messages to be broadcasted. + # + # def test_broadcasts_again + # assert_broadcasts('messages', 1) do + # ActionCable.server.broadcast 'messages', { text: 'hello' } + # end + # + # assert_broadcasts('messages', 2) do + # ActionCable.server.broadcast 'messages', { text: 'hi' } + # ActionCable.server.broadcast 'messages', { text: 'how are you?' } + # end + # end + # + def assert_broadcasts(stream, number) + if block_given? + original_count = broadcasts_size(stream) + yield + new_count = broadcasts_size(stream) + actual_count = new_count - original_count + else + actual_count = broadcasts_size(stream) + end + + assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent" + end + + # Asserts that no messages have been sent to the stream. + # + # def test_no_broadcasts + # assert_no_broadcasts 'messages' + # ActionCable.server.broadcast 'messages', { text: 'hi' } + # assert_broadcasts 'messages', 1 + # end + # + # If a block is passed, that block should not cause any message to be sent. + # + # def test_broadcasts_again + # assert_no_broadcasts 'messages' do + # # No job messages should be sent from this block + # end + # end + # + # Note: This assertion is simply a shortcut for: + # + # assert_broadcasts 'messages', 0, &block + # + def assert_no_broadcasts(stream, &block) + assert_broadcasts stream, 0, &block + end + + # Asserts that the specified message has been sent to the stream. + # + # def test_assert_transmited_message + # ActionCable.server.broadcast 'messages', text: 'hello' + # assert_broadcast_on('messages', text: 'hello') + # end + # + # If a block is passed, that block should cause a message with the specified data to be sent. + # + # def test_assert_broadcast_on_again + # assert_broadcast_on('messages', text: 'hello') do + # ActionCable.server.broadcast 'messages', text: 'hello' + # end + # end + # + def assert_broadcast_on(stream, data) + # Encode to JSON and back–we want to use this value to compare + # with decoded JSON. + # Comparing JSON strings doesn't work due to the order if the keys. + serialized_msg = + ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data)) + + new_messages = broadcasts(stream) + if block_given? + old_messages = new_messages + clear_messages(stream) + + yield + new_messages = broadcasts(stream) + clear_messages(stream) + + # Restore all sent messages + (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) } + end + + message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg } + + assert message, "No messages sent with #{data} to #{stream}" + end + + def pubsub_adapter # :nodoc: + ActionCable.server.pubsub + end + + delegate :broadcasts, :clear_messages, to: :pubsub_adapter + + private + def broadcasts_size(channel) + broadcasts(channel).size + end + end +end diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE index dd109fda80..ea9662436c 100644 --- a/actioncable/lib/rails/generators/channel/USAGE +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -1,14 +1,12 @@ Description: ============ - Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript). + Stubs out a new cable channel for the server (in Ruby) and client (in JavaScript). Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments. - Note: Turn on the cable connection in app/assets/javascripts/cable.js after generating any channels. - Example: ======== rails generate channel Chat speak - creates a Chat channel class and CoffeeScript asset: + creates a Chat channel class and JavaScript asset: Channel: app/channels/chat_channel.rb - Assets: app/assets/javascripts/channels/chat.coffee + Assets: app/javascript/channels/chat_channel.js diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 427eef1f55..ef51981e89 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -16,10 +16,11 @@ module Rails if options[:assets] if behavior == :invoke - template "assets/cable.js", "app/assets/javascripts/cable.js" + template "javascript/index.js", "app/javascript/channels/index.js" + template "javascript/consumer.js", "app/javascript/channels/consumer.js" end - js_template "assets/channel", File.join("app/assets/javascripts/channels", class_path, "#{file_name}") + js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel") end generate_application_cable_files diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt deleted file mode 100644 index 5467811aba..0000000000 --- a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt +++ /dev/null @@ -1,14 +0,0 @@ -App.<%= class_name.underscore %> = App.cable.subscriptions.create "<%= class_name %>Channel", - connected: -> - # Called when the subscription is ready for use on the server - - disconnected: -> - # Called when the subscription has been terminated by the server - - received: (data) -> - # Called when there's incoming data on the websocket for this channel -<% actions.each do |action| -%> - - <%= action %>: -> - @perform '<%= action %>' -<% end -%> diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt index ab0e68b11a..33baaa5a22 100644 --- a/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt +++ b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt @@ -1,4 +1,6 @@ -App.<%= class_name.underscore %> = App.cable.subscriptions.create("<%= class_name %>Channel", { +import consumer from "./consumer" + +consumer.subscriptions.create("<%= class_name %>Channel", { connected: function() { // Called when the subscription is ready for use on the server }, diff --git a/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt index 739aa5f022..76ca3d0f2f 100644 --- a/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt +++ b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt @@ -1,13 +1,6 @@ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. -// -//= require action_cable -//= require_self -//= require_tree ./channels -(function() { - this.App || (this.App = {}); +import ActionCable from "actioncable" - App.cable = ActionCable.createConsumer(); - -}).call(this); +export default ActionCable.createConsumer() diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt new file mode 100644 index 0000000000..0cfcf74919 --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/) +channels.keys().forEach(channels) diff --git a/actioncable/package.json b/actioncable/package.json index d9df46043d..5d5105a6d5 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -2,9 +2,9 @@ "name": "actioncable", "version": "6.0.0-alpha", "description": "WebSocket framework for Ruby on Rails.", - "main": "lib/assets/compiled/action_cable.js", + "main": "app/assets/javascripts/action_cable.js", "files": [ - "lib/assets/compiled/*.js" + "app/assets/javascripts/*.js" ], "repository": { "type": "git", @@ -20,5 +20,22 @@ "bugs": { "url": "https://github.com/rails/rails/issues" }, - "homepage": "http://rubyonrails.org/" + "homepage": "http://rubyonrails.org/", + "devDependencies": { + "babel-core": "^6.25.0", + "babel-plugin-external-helpers": "^6.22.0", + "babel-preset-env": "^1.6.0", + "eslint": "^4.3.0", + "eslint-plugin-import": "^2.7.0", + "rollup": "^0.58.2", + "rollup-plugin-babel": "^3.0.4", + "rollup-plugin-commonjs": "^9.1.0", + "rollup-plugin-node-resolve": "^3.3.0", + "rollup-plugin-uglify": "^3.0.0" + }, + "scripts": { + "prebuild": "yarn lint && bundle exec rake assets:codegen", + "build": "rollup --config rollup.config.js", + "lint": "eslint app/javascript" + } } diff --git a/actioncable/rollup.config.js b/actioncable/rollup.config.js new file mode 100644 index 0000000000..03046526aa --- /dev/null +++ b/actioncable/rollup.config.js @@ -0,0 +1,28 @@ +import resolve from "rollup-plugin-node-resolve" +import commonjs from "rollup-plugin-commonjs" +import babel from "rollup-plugin-babel" +import uglify from "rollup-plugin-uglify" + +const uglifyOptions = { + mangle: false, + compress: false, + output: { + beautify: true, + indent_level: 2 + } +} + +export default { + input: "app/javascript/action_cable/index.js", + output: { + file: "app/assets/javascripts/action_cable.js", + format: "umd", + name: "ActionCable" + }, + plugins: [ + resolve(), + commonjs(), + babel(), + uglify(uglifyOptions) + ] +} diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index bfe1f92946..9ad2213d47 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -26,16 +26,17 @@ module ActionCable::StreamTests transmit_subscription_confirmation end - private def pick_coder(coder) - case coder - when nil, "json" - ActiveSupport::JSON - when "custom" - DummyEncoder - when "none" - nil + private + def pick_coder(coder) + case coder + when nil, "json" + ActiveSupport::JSON + when "custom" + DummyEncoder + when "none" + nil + end end - end end module DummyEncoder @@ -192,7 +193,7 @@ module ActionCable::StreamTests end end - test "subscription confirmation should only be sent out once with muptiple stream_from" do + test "subscription confirmation should only be sent out once with multiple stream_from" do run_in_eventmachine do connection = open_connection expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" } diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb new file mode 100644 index 0000000000..63d0d6207e --- /dev/null +++ b/actioncable/test/channel/test_case_test.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestTestChannel < ActionCable::Channel::Base +end + +class NonInferrableExplicitClassChannelTest < ActionCable::Channel::TestCase + tests TestTestChannel + + def test_set_channel_class_manual + assert_equal TestTestChannel, self.class.channel_class + end +end + +class NonInferrableSymbolNameChannelTest < ActionCable::Channel::TestCase + tests :test_test_channel + + def test_set_channel_class_manual_using_symbol + assert_equal TestTestChannel, self.class.channel_class + end +end + +class NonInferrableStringNameChannelTest < ActionCable::Channel::TestCase + tests "test_test_channel" + + def test_set_channel_class_manual_using_string + assert_equal TestTestChannel, self.class.channel_class + end +end + +class SubscriptionsTestChannel < ActionCable::Channel::Base +end + +class SubscriptionsTestChannelTest < ActionCable::Channel::TestCase + def setup + stub_connection + end + + def test_no_subscribe + assert_nil subscription + end + + def test_subscribe + subscribe + + assert subscription.confirmed? + assert_not subscription.rejected? + assert_equal 1, connection.transmissions.size + assert_equal ActionCable::INTERNAL[:message_types][:confirmation], + connection.transmissions.last["type"] + end +end + +class StubConnectionTest < ActionCable::Channel::TestCase + tests SubscriptionsTestChannel + + def test_connection_identifiers + stub_connection username: "John", admin: true + + subscribe + + assert_equal "John", subscription.username + assert subscription.admin + end +end + +class RejectionTestChannel < ActionCable::Channel::Base + def subscribed + reject + end +end + +class RejectionTestChannelTest < ActionCable::Channel::TestCase + def test_rejection + subscribe + + assert_not subscription.confirmed? + assert subscription.rejected? + assert_equal 1, connection.transmissions.size + assert_equal ActionCable::INTERNAL[:message_types][:rejection], + connection.transmissions.last["type"] + end +end + +class StreamsTestChannel < ActionCable::Channel::Base + def subscribed + stream_from "test_#{params[:id] || 0}" + end +end + +class StreamsTestChannelTest < ActionCable::Channel::TestCase + def test_stream_without_params + subscribe + + assert_equal "test_0", streams.last + end + + def test_stream_with_params + subscribe id: 42 + + assert_equal "test_42", streams.last + end +end + +class PerformTestChannel < ActionCable::Channel::Base + def echo(data) + data.delete("action") + transmit data + end + + def ping + transmit type: "pong" + end +end + +class PerformTestChannelTest < ActionCable::Channel::TestCase + def setup + stub_connection user_id: 2016 + subscribe id: 5 + end + + def test_perform_with_params + perform :echo, text: "You are man!" + + assert_equal({ "text" => "You are man!" }, transmissions.last) + end + + def test_perform_and_transmit + perform :ping + + assert_equal "pong", transmissions.last["type"] + end +end + +class PerformUnsubscribedTestChannelTest < ActionCable::Channel::TestCase + tests PerformTestChannel + + def test_perform_when_unsubscribed + assert_raises do + perform :echo + end + end +end + +class BroadcastsTestChannel < ActionCable::Channel::Base + def broadcast(data) + ActionCable.server.broadcast( + "broadcast_#{params[:id]}", + text: data["message"], user_id: user_id + ) + end + + def broadcast_to_user(data) + user = User.new user_id + + self.class.broadcast_to user, text: data["message"] + end +end + +class BroadcastsTestChannelTest < ActionCable::Channel::TestCase + def setup + stub_connection user_id: 2017 + subscribe id: 5 + end + + def test_broadcast_matchers_included + assert_broadcast_on("broadcast_5", user_id: 2017, text: "SOS") do + perform :broadcast, message: "SOS" + end + end + + def test_broadcast_to_object + user = User.new(2017) + + assert_broadcasts(user, 1) do + perform :broadcast_to_user, text: "SOS" + end + end + + def test_broadcast_to_object_with_data + user = User.new(2017) + + assert_broadcast_on(user, text: "SOS") do + perform :broadcast_to_user, message: "SOS" + end + end +end diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 3dc995331a..ac2d8ef724 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -30,14 +30,22 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest end class RedisAdapterTest::Connector < ActionCable::TestCase - test "slices url, host, port, db, and password from config" do - config = { url: 1, host: 2, port: 3, db: 4, password: 5 } + test "slices url, host, port, db, password and id from config" do + config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "Some custom ID" } assert_called_with ::Redis, :new, [ config ] do connect config.merge(other: "unrelated", stuff: "here") end end + test "adds default id if it is not specified" do + config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "ActionCable-PID-#{$$}" } + + assert_called_with ::Redis, :new, [ config ] do + connect config.except(:id) + end + end + def connect(config) ActionCable::SubscriptionAdapter::Redis.redis_connector.call(config) end diff --git a/actioncable/test/subscription_adapter/test_adapter_test.rb b/actioncable/test/subscription_adapter/test_adapter_test.rb new file mode 100644 index 0000000000..3fe07adb4a --- /dev/null +++ b/actioncable/test/subscription_adapter/test_adapter_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "common" + +class ActionCable::SubscriptionAdapter::TestTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + @tx_adapter.shutdown + @tx_adapter = @rx_adapter + end + + def cable_config + { adapter: "test" } + end + + test "#broadcast stores messages for streams" do + @tx_adapter.broadcast("channel", "payload") + @tx_adapter.broadcast("channel2", "payload2") + + assert_equal ["payload"], @tx_adapter.broadcasts("channel") + assert_equal ["payload2"], @tx_adapter.broadcasts("channel2") + end + + test "#clear_messages deletes recorded broadcasts for the channel" do + @tx_adapter.broadcast("channel", "payload") + @tx_adapter.broadcast("channel2", "payload2") + + @tx_adapter.clear_messages("channel") + + assert_equal [], @tx_adapter.broadcasts("channel") + assert_equal ["payload2"], @tx_adapter.broadcasts("channel2") + end + + test "#clear deletes all recorded broadcasts" do + @tx_adapter.broadcast("channel", "payload") + @tx_adapter.broadcast("channel2", "payload2") + + @tx_adapter.clear + + assert_equal [], @tx_adapter.broadcasts("channel") + assert_equal [], @tx_adapter.broadcasts("channel2") + end +end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index ac7881c950..c924f1e475 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -15,6 +15,10 @@ end # Require all the stubs and models Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file } +# Set test adapter and logger +ActionCable.server.config.cable = { "adapter" => "test" } +ActionCable.server.config.logger = Logger.new(nil) + class ActionCable::TestCase < ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions diff --git a/actioncable/test/test_helper_test.rb b/actioncable/test/test_helper_test.rb new file mode 100644 index 0000000000..90e3dbf01f --- /dev/null +++ b/actioncable/test/test_helper_test.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "test_helper" + +class BroadcastChannel < ActionCable::Channel::Base +end + +class TransmissionsTest < ActionCable::TestCase + def test_assert_broadcasts + assert_nothing_raised do + assert_broadcasts("test", 1) do + ActionCable.server.broadcast "test", "message" + end + end + end + + def test_assert_broadcasts_with_no_block + assert_nothing_raised do + ActionCable.server.broadcast "test", "message" + assert_broadcasts "test", 1 + end + + assert_nothing_raised do + ActionCable.server.broadcast "test", "message 2" + ActionCable.server.broadcast "test", "message 3" + assert_broadcasts "test", 3 + end + end + + def test_assert_no_broadcasts_with_no_block + assert_nothing_raised do + assert_no_broadcasts "test" + end + end + + def test_assert_no_broadcasts + assert_nothing_raised do + assert_no_broadcasts("test") do + ActionCable.server.broadcast "test2", "message" + end + end + end + + def test_assert_broadcasts_message_too_few_sent + ActionCable.server.broadcast "test", "hello" + error = assert_raises Minitest::Assertion do + assert_broadcasts("test", 2) do + ActionCable.server.broadcast "test", "world" + end + end + + assert_match(/2 .* but 1/, error.message) + end + + def test_assert_broadcasts_message_too_many_sent + error = assert_raises Minitest::Assertion do + assert_broadcasts("test", 1) do + ActionCable.server.broadcast "test", "hello" + ActionCable.server.broadcast "test", "world" + end + end + + assert_match(/1 .* but 2/, error.message) + end + + def test_assert_no_broadcasts_failure + error = assert_raises Minitest::Assertion do + assert_no_broadcasts "test" do + ActionCable.server.broadcast "test", "hello" + end + end + + assert_match(/0 .* but 1/, error.message) + end +end + +class TransmitedDataTest < ActionCable::TestCase + include ActionCable::TestHelper + + def test_assert_broadcast_on + assert_nothing_raised do + assert_broadcast_on("test", "message") do + ActionCable.server.broadcast "test", "message" + end + end + end + + def test_assert_broadcast_on_with_hash + assert_nothing_raised do + assert_broadcast_on("test", text: "hello") do + ActionCable.server.broadcast "test", text: "hello" + end + end + end + + def test_assert_broadcast_on_with_no_block + assert_nothing_raised do + ActionCable.server.broadcast "test", "hello" + assert_broadcast_on "test", "hello" + end + + assert_nothing_raised do + ActionCable.server.broadcast "test", "world" + assert_broadcast_on "test", "world" + end + end + + def test_assert_broadcast_on_message + ActionCable.server.broadcast "test", "hello" + error = assert_raises Minitest::Assertion do + assert_broadcast_on("test", "world") + end + + assert_match(/No messages sent/, error.message) + end +end diff --git a/actioncable/yarn.lock b/actioncable/yarn.lock new file mode 100644 index 0000000000..8b4fcaebd6 --- /dev/null +++ b/actioncable/yarn.lock @@ -0,0 +1,1928 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/estree@0.0.38": + version "0.0.38" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2" + +"@types/node@*": + version "10.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.6.tgz#ce5690df6cd917a9178439a1013e39a7e565c46e" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^5.2.1: + version "5.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv@^5.2.3, ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ansi-escapes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.25.0, babel-core@^6.26.0: + version "6.26.3" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.1" + debug "^2.6.9" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.8" + slash "^1.0.0" + source-map "^0.5.7" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-external-helpers@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-env@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^3.2.6" + invariant "^2.2.2" + semver "^5.3.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +browserslist@^3.2.6: + version "3.2.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" + dependencies: + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" + +builtin-modules@^1.0.0, builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +caniuse-lite@^1.0.30000844: + version "1.0.30000890" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000890.tgz#86a18ffcc65d79ec6a437e985761b8bf1c4efeaf" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chardet@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + +convert-source-map@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + dependencies: + safe-buffer "~5.1.1" + +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + dependencies: + esutils "^2.0.2" + +electron-to-chromium@^1.3.47: + version "1.3.75" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.75.tgz#dd04551739e7371862b0ac7f4ddaa9f3f95b7e68" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +eslint-import-resolver-node@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" + dependencies: + debug "^2.6.9" + resolve "^1.5.0" + +eslint-module-utils@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449" + dependencies: + debug "^2.6.8" + pkg-dir "^1.0.0" + +eslint-plugin-import@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894" + dependencies: + builtin-modules "^1.1.1" + contains-path "^0.1.0" + debug "^2.6.8" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.1.1" + has "^1.0.1" + lodash.cond "^4.3.0" + minimatch "^3.0.3" + read-pkg-up "^2.0.0" + +eslint-scope@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +eslint@^4.3.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.2" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "^4.0.1" + text-table "~0.2.0" + +espree@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" + dependencies: + acorn "^5.2.1" + acorn-jsx "^3.0.0" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estree-walker@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" + +estree-walker@^0.5.1, estree-walker@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +external-editor@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^3.0.0" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +function-bind@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.0.1: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4" + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +iconv-lite@^0.4.17: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +ignore@^3.3.3: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-resolvable@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.1.tgz#acca1cd36dbe44b974b924321555a70ba03b1cf4" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.9.1: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.cond@^4.3.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" + +lodash@^4.17.4, lodash@^4.3.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +magic-string@^0.22.4: + version "0.22.5" + resolved "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" + dependencies: + vlq "^0.2.2" + +math-random@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" + +micromatch@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +private@^0.1.6, private@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +randomatic@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.0.tgz#36f2ca708e9e567f5ed2ec01949026d50aa10116" + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +readable-stream@^2.2.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +regenerate@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@^1.1.6: + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + dependencies: + path-parse "^1.0.5" + +resolve@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rimraf@^2.2.8: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +rollup-plugin-babel@^3.0.4: + version "3.0.7" + resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.7.tgz#5b13611f1ab8922497e9d15197ae5d8a23fe3b1e" + dependencies: + rollup-pluginutils "^1.5.0" + +rollup-plugin-commonjs@^9.1.0: + version "9.1.8" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.8.tgz#4113ed94e6054b5f8a3501d8811f934cadde3246" + dependencies: + estree-walker "^0.5.1" + magic-string "^0.22.4" + resolve "^1.5.0" + rollup-pluginutils "^2.0.1" + +rollup-plugin-node-resolve@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz#908585eda12e393caac7498715a01e08606abc89" + dependencies: + builtin-modules "^2.0.0" + is-module "^1.0.0" + resolve "^1.1.6" + +rollup-plugin-uglify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-3.0.0.tgz#a34eca24617709c6bf1778e9653baafa06099b86" + dependencies: + uglify-es "^3.3.7" + +rollup-pluginutils@^1.5.0: + version "1.5.2" + resolved "http://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" + dependencies: + estree-walker "^0.2.1" + minimatch "^3.0.2" + +rollup-pluginutils@^2.0.1: + version "2.3.3" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794" + dependencies: + estree-walker "^0.5.2" + micromatch "^2.3.11" + +rollup@^0.58.2: + version "0.58.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.58.2.tgz#2feddea8c0c022f3e74b35c48e3c21b3433803ce" + dependencies: + "@types/estree" "0.0.38" + "@types/node" "*" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +table@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" + dependencies: + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-es@^3.3.7: + version "3.3.9" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vlq@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 1468a89e96..db6fc7ee9c 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,26 @@ +* Allow ActionMailer classes to configure the parameterized delivery job + Example: + ``` + class MyMailer < ApplicationMailer + self.parameterized_delivery_job = MyCustomDeliveryJob + ... + end + ``` + + *Luke Pearce* + +* `ActionDispatch::IntegrationTest` includes `ActionMailer::TestHelper` module by default. + + *Ricardo Díaz* + +* Add `perform_deliveries` to a payload of `deliver.action_mailer` notification. + + *Yoshiyuki Kinjo* + +* Change delivery logging message when `perform_deliveries` is false. + + *Yoshiyuki Kinjo* + * Allow call `assert_enqueued_email_with` with no block. Example: diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index f2fb160bdd..6f8d6d0699 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -26,6 +26,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailer/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "actionpack", version s.add_dependency "actionview", version s.add_dependency "activejob", version diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 7f22af83b0..509d859ac3 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -462,6 +462,7 @@ module ActionMailer helper ActionMailer::MailHelper class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob + class_attribute :parameterized_delivery_job, default: ::ActionMailer::Parameterized::DeliveryJob class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", @@ -588,15 +589,16 @@ module ActionMailer private def set_payload_for_mail(payload, mail) - payload[:mailer] = name - payload[:message_id] = mail.message_id - payload[:subject] = mail.subject - payload[:to] = mail.to - payload[:from] = mail.from - payload[:bcc] = mail.bcc if mail.bcc.present? - payload[:cc] = mail.cc if mail.cc.present? - payload[:date] = mail.date - payload[:mail] = mail.encoded + payload[:mailer] = name + payload[:message_id] = mail.message_id + payload[:subject] = mail.subject + payload[:to] = mail.to + payload[:from] = mail.from + payload[:bcc] = mail.bcc if mail.bcc.present? + payload[:cc] = mail.cc if mail.cc.present? + payload[:date] = mail.date + payload[:mail] = mail.encoded + payload[:perform_deliveries] = mail.perform_deliveries end def method_missing(method_name, *args) @@ -1007,7 +1009,7 @@ module ActionMailer end def instrument_name - "action_mailer".freeze + "action_mailer" end ActiveSupport.run_load_hooks(:action_mailer, self) diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 87cfbfff28..25c99342c2 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -9,8 +9,13 @@ module ActionMailer # An email was delivered. def deliver(event) info do + perform_deliveries = event.payload[:perform_deliveries] recipients = Array(event.payload[:to]).join(", ") - "Sent mail to #{recipients} (#{event.duration.round(1)}ms)" + if perform_deliveries + "Sent mail to #{recipients} (#{event.duration.round(1)}ms)" + else + "Skipped sending mail to #{recipients} as `perform_deliveries` is false" + end end debug { event.payload[:mail] } diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index 2377aeb9a5..a57980c322 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -29,7 +29,7 @@ module ActionMailer @mail_message ||= processed_mailer.message end - # Unused except for delegator internals (dup, marshaling). + # Unused except for delegator internals (dup, marshalling). def __setobj__(mail_message) #:nodoc: @mail_message = mail_message end diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb index 5e768e7106..0fe417affe 100644 --- a/actionmailer/lib/action_mailer/parameterized.rb +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -140,7 +140,8 @@ module ActionMailer super else args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args - ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args) + job = @mailer_class.parameterized_delivery_job + job.set(options).perform_later(*args) end end end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 69578471b0..bb6141b406 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -49,7 +49,10 @@ module ActionMailer options.each { |k, v| send("#{k}=", v) } end - ActiveSupport.on_load(:action_dispatch_integration_test) { include ActionMailer::TestCase::ClearTestDeliveries } + ActiveSupport.on_load(:action_dispatch_integration_test) do + include ActionMailer::TestHelper + include ActionMailer::TestCase::ClearTestDeliveries + end end initializer "action_mailer.compile_config_methods" do diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 45f69d5375..f647896374 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -36,12 +36,14 @@ ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb index eb58ddd9c9..9699fe4000 100644 --- a/actionmailer/test/assert_select_email_test.rb +++ b/actionmailer/test/assert_select_email_test.rb @@ -25,7 +25,7 @@ class AssertSelectEmailTest < ActionMailer::TestCase def test_assert_select_email assert_raise ActiveSupport::TestCase::Assertion do - assert_select_email {} + assert_select_email { } end AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver_now diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 7898996c30..cbbee9fae8 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -122,7 +122,7 @@ class BaseTest < ActiveSupport::TestCase email = BaseMailer.attachment_with_hash assert_equal(1, email.attachments.length) assert_equal("invoice.jpg", email.attachments[0].filename) - expected = "\312\213\254\232)b".dup + expected = +"\312\213\254\232)b" expected.force_encoding(Encoding::BINARY) assert_equal expected, email.attachments["invoice.jpg"].decoded end @@ -131,7 +131,7 @@ class BaseTest < ActiveSupport::TestCase email = BaseMailer.attachment_with_hash_default_encoding assert_equal(1, email.attachments.length) assert_equal("invoice.jpg", email.attachments[0].filename) - expected = "\312\213\254\232)b".dup + expected = +"\312\213\254\232)b" expected.force_encoding(Encoding::BINARY) assert_equal expected, email.attachments["invoice.jpg"].decoded end @@ -152,9 +152,9 @@ class BaseTest < ActiveSupport::TestCase assert_equal(2, email.parts.length) assert_equal("multipart/mixed", email.mime_type) assert_equal("text/html", email.parts[0].mime_type) - assert_equal("Attachment with content", email.parts[0].body.encoded) + assert_equal("Attachment with content", email.parts[0].decoded) assert_equal("application/pdf", email.parts[1].mime_type) - assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) + assert_equal("This is test File content", email.parts[1].decoded) end test "adds the given :body as part" do @@ -162,9 +162,9 @@ class BaseTest < ActiveSupport::TestCase assert_equal(2, email.parts.length) assert_equal("multipart/mixed", email.mime_type) assert_equal("text/plain", email.parts[0].mime_type) - assert_equal("I'm the eggman", email.parts[0].body.encoded) + assert_equal("I'm the eggman", email.parts[0].decoded) assert_equal("application/pdf", email.parts[1].mime_type) - assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) + assert_equal("This is test File content", email.parts[1].decoded) end test "can embed an inline attachment" do diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index 2e89758dfb..7686fd10c9 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -37,6 +37,20 @@ class AMLogSubscriberTest < ActionMailer::TestCase BaseMailer.deliveries.clear end + def test_deliver_message_when_perform_deliveries_is_false + BaseMailer.welcome_without_deliveries.deliver_now + wait + + assert_equal(1, @logger.logged(:info).size) + assert_match("Skipped sending mail to system@test.lindsaar.net as `perform_deliveries` is false", @logger.logged(:info).first) + + assert_equal(2, @logger.logged(:debug).size) + assert_match(/BaseMailer#welcome_without_deliveries: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first) + assert_match("Welcome", @logger.logged(:debug).second) + ensure + BaseMailer.deliveries.clear + end + def test_receive_is_notified fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) TestMailer.receive(fixture) diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index bfaecdb658..a3101207dc 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -21,6 +21,11 @@ class BaseMailer < ActionMailer::Base mail(template_name: "welcome", template_path: path) end + def welcome_without_deliveries + mail(template_name: "welcome") + mail.perform_deliveries = false + end + def html_only(hash = {}) mail(hash) end diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb index ec6c5e9e67..d3b34fc3e3 100644 --- a/actionmailer/test/parameterized_test.rb +++ b/actionmailer/test/parameterized_test.rb @@ -53,4 +53,17 @@ class ParameterizedTest < ActiveSupport::TestCase invitation = mailer.method(:anything) end end + + test "should enqueue a parameterized request with the correct delivery job" do + old_delivery_job = ParamsMailer.parameterized_delivery_job + ParamsMailer.parameterized_delivery_job = ParameterizedDummyJob + + assert_performed_with(job: ParameterizedDummyJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do + @mail.deliver_later + end + + ParamsMailer.parameterized_delivery_job = old_delivery_job + end + + class ParameterizedDummyJob < ActionMailer::Parameterized::DeliveryJob; end end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index dcea23aabb..d31170706b 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -290,7 +290,7 @@ class TestHelperMailerTest < ActionMailer::TestCase end end - def test_assert_enqueued_email_with_with_no_block_wiht_parameterized_args + def test_assert_enqueued_email_with_with_no_block_with_parameterized_args assert_nothing_raised do silence_stream($stdout) do TestHelperMailer.with(all: "good").test_parameter_args.deliver_later diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 3c940bc969..a926663a9f 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -8,11 +8,16 @@ end AppRoutes = ActionDispatch::Routing::RouteSet.new -class ActionMailer::Base - include AppRoutes.url_helpers +AppRoutes.draw do + get "/welcome" => "foo#bar", as: "welcome" + get "/dummy_model" => "foo#baz", as: "dummy_model" + get "/welcome/greeting", to: "welcome#greeting" + get "/a/b(/:id)", to: "a#b" end class UrlTestMailer < ActionMailer::Base + include AppRoutes.url_helpers + default_url_options[:host] = "www.basecamphq.com" configure do |c| @@ -80,14 +85,6 @@ class ActionMailerUrlTest < ActionMailer::TestCase def test_url_for UrlTestMailer.delivery_method = :test - AppRoutes.draw do - ActiveSupport::Deprecation.silence do - get ":controller(/:action(/:id))" - get "/welcome" => "foo#bar", as: "welcome" - get "/dummy_model" => "foo#baz", as: "dummy_model" - end - end - # string assert_url_for "http://foo/", "http://foo/" @@ -111,13 +108,6 @@ class ActionMailerUrlTest < ActionMailer::TestCase def test_signed_up_with_url UrlTestMailer.delivery_method = :test - AppRoutes.draw do - ActiveSupport::Deprecation.silence do - get ":controller(/:action(/:id))" - get "/welcome" => "foo#bar", as: "welcome" - end - end - expected = new_mail expected.to = @recipient expected.subject = "[Signed up] Welcome #{@recipient}" diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index a5497aa055..81201795d4 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,73 @@ +* Use request object for context if there's no controller + + There is no controller instance when using a redirect route or a + mounted rack application so pass the request object as the context + when resolving dynamic CSP sources in this scenario. + + Fixes #34200. + + *Andrew White* + +* Apply mapping to symbols returned from dynamic CSP sources + + Previously if a dynamic source returned a symbol such as :self it + would be converted to a string implicity, e.g: + + policy.default_src -> { :self } + + would generate the header: + + Content-Security-Policy: default-src self + + and now it generates: + + Content-Security-Policy: default-src 'self' + + *Andrew White* + +* Add `ActionController::Parameters#each_value`. + + *Lukáš Zapletal* + +* Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`. + + *Yoshiyuki Kinjo* + +* Remove undocumented `params` option from `url_for` helper. + + *Ilkka Oksanen* + +* Encode Content-Disposition filenames on `send_data` and `send_file`. + Previously, `send_data 'data', filename: "\u{3042}.txt"` sends + `"filename=\"\u{3042}.txt\""` as Content-Disposition and it can be + garbled. + Now it follows [RFC 2231](https://tools.ietf.org/html/rfc2231) and + [RFC 5987](https://tools.ietf.org/html/rfc5987) and sends + `"filename=\"%3F.txt\"; filename*=UTF-8''%E3%81%82.txt"`. + Most browsers can find filename correctly and old browsers fallback to ASCII + converted name. + + *Fumiaki Matsushima* + +* Expose `ActionController::Parameters#each_key` which allows iterating over + keys without allocating an array. + + *Richard Schneeman* + +* Purpose metadata for signed/encrypted cookies. + + Rails can now thwart attacks that attempt to copy signed/encrypted value + of a cookie and use it as the value of another cookie. + + It does so by stashing the cookie-name in the purpose field which is + then signed/encrypted along with the cookie value. Then, on a server-side + read, we verify the cookie-names and discard any attacked cookies. + + Enable `action_dispatch.use_cookies_with_metadata` to use this feature, which + writes cookies with the new purpose and expiry metadata embedded. + + *Assain Jaleel* + * Raises `ActionController::RespondToMismatchError` with confliciting `respond_to` invocations. `respond_to` can match multiple types and lead to undefined behavior when @@ -23,7 +93,7 @@ *Aaron Kromer* -* Pass along arguments to underlying `get` method in `follow_redirect!` +* Pass along arguments to underlying `get` method in `follow_redirect!`. Now all arguments passed to `follow_redirect!` are passed to the underlying `get` method. This for example allows to set custom headers for the @@ -40,7 +110,7 @@ *Vinicius Stock* -* Introduce ActionDispatch::DebugExceptions.register_interceptor +* Introduce `ActionDispatch::DebugExceptions.register_interceptor`. Exception aware plugin authors can use the newly introduced `.register_interceptor` method to get the processed exception, instead of diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 1dc8abf746..ec56de18f1 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -26,6 +26,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionpack/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "rack", "~> 2.0" diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index a312af6715..bb42f2e119 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -78,7 +78,9 @@ module AbstractController # Except for public instance methods of Base and its ancestors internal_methods + # Be sure to include shadowed public instance methods of this class - public_instance_methods(false)).uniq.map(&:to_s) + public_instance_methods(false)) + + methods.map!(&:to_s) methods.to_set end @@ -102,7 +104,7 @@ module AbstractController # ==== Returns # * <tt>String</tt> def controller_path - @controller_path ||= name.sub(/Controller$/, "".freeze).underscore unless anonymous? + @controller_path ||= name.sub(/Controller$/, "").underscore unless anonymous? end # Refresh the cached action_methods when a new action_method is added. diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index f99b0830b2..95078a2a28 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -82,13 +82,17 @@ module AbstractController # Given a key (as described in +expire_fragment+), returns # a key array suitable for use in reading, writing, or expiring a # cached fragment. All keys begin with <tt>:views</tt>, - # followed by ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] if set, + # followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set, # followed by any controller-wide key prefix values, ending # with the specified +key+ value. def combined_fragment_cache_key(key) head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key - [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact + + cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail] + cache_key.flatten!(1) + cache_key.compact! + cache_key end # Writes +content+ to the location signified by diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 3191584770..3913259ecc 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -181,7 +181,7 @@ module AbstractController end def default_helper_module! - module_name = name.sub(/Controller$/, "".freeze) + module_name = name.sub(/Controller$/, "") module_path = module_name.underscore helper module_path rescue LoadError => e diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index b6e5631a4e..fbd93705ed 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -7,7 +7,7 @@ module AbstractController Module.new do define_method(:inherited) do |klass| super(klass) - if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } + if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } klass.include(namespace.railtie_routes_url_helpers(include_path_helpers)) else klass.include(routes.url_helpers(include_path_helpers)) diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 93ffff1bd6..c276ee57c0 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -12,7 +12,7 @@ module ActionController # # An API Controller is different from a normal controller in the sense that # by default it doesn't include a number of features that are usually required - # by browser access only: layouts and templates rendering, cookies, sessions, + # by browser access only: layouts and templates rendering, # flash, assets, and so on. This makes the entire controller stack thinner, # suitable for API applications. It doesn't mean you won't have such # features if you need them: they're all available for you to include in diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 97775d1dc8..bf3b00a7b7 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -40,7 +40,7 @@ module ActionController end def instrument_name - "action_controller".freeze + "action_controller" end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 14f41eb55f..afbd38e7fe 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -18,16 +18,19 @@ module ActionController def process_action(event) info do - payload = event.payload + payload = event.payload additions = ActionController::Base.log_process_action(payload) - status = payload[:status] + if status.nil? && payload[:exception].present? exception_class_name = payload[:exception].first status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end - message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms".dup - message << " (#{additions.join(" | ".freeze)})" unless additions.empty? + + additions << "Allocations: #{event.allocations}" + + message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" + message << " (#{additions.join(" | ")})" unless additions.empty? message << "\n\n" if defined?(Rails.env) && Rails.env.development? message diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 4be4557e2c..d6911ee2b5 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -230,6 +230,12 @@ module ActionController # This method will overwrite an existing Cache-Control header. # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # + # HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861 + # It helps to cache an asset and serve it while is being revalidated and/or returning with an error. + # + # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds + # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes + # # The method will also ensure an HTTP Date header for client compatibility. def expires_in(seconds, options = {}) response.cache_control.merge!( diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 5a82ccf668..9ef4f50df1 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "action_controller/metal/exceptions" +require "action_dispatch/http/content_disposition" module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, @@ -10,8 +11,8 @@ module ActionController #:nodoc: include ActionController::Rendering - DEFAULT_SEND_FILE_TYPE = "application/octet-stream".freeze #:nodoc: - DEFAULT_SEND_FILE_DISPOSITION = "attachment".freeze #:nodoc: + DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc: + DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc: private # Sends the file. This uses a server-appropriate method (such as X-Sendfile) @@ -132,10 +133,8 @@ module ActionController #:nodoc: end disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) - unless disposition.nil? - disposition = disposition.to_s - disposition += %(; filename="#{options[:filename]}") if options[:filename] - headers["Content-Disposition"] = disposition + if disposition + headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename]) end headers["Content-Transfer-Encoding"] = "binary" diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 5115c2fadf..380f2e9591 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -36,7 +36,7 @@ module ActionController #:nodoc: define_method(type) do request.flash[type] end - helper_method type + helper_method(type) if respond_to?(:helper_method) self._flash_types += [type] end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 8d53a30e93..26e6f72b66 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -5,8 +5,8 @@ require "active_support/core_ext/hash/slice" module ActionController # This module is deprecated in favor of +config.force_ssl+ in your environment - # config file. This will ensure all communication to non-whitelisted endpoints - # served by your application occurs over HTTPS. + # config file. This will ensure all endpoints not explicitly marked otherwise + # will have all communication served over HTTPS. module ForceSSL # :nodoc: extend ActiveSupport::Concern include AbstractController::Callbacks diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 22c84e440b..0faaac1ce4 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -100,8 +100,7 @@ module ActionController # # => ["application", "chart", "rubygems"] def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| - extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] } names.sort! end helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index a871ccd533..7036123d5d 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -127,7 +127,7 @@ module ActionController def authentication_request(controller, realm, message) message ||= "HTTP Basic: Access denied.\n" - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}") controller.status = 401 controller.response_body = message end @@ -474,7 +474,7 @@ module ActionController # This removes the <tt>"</tt> characters wrapping the value. def rewrite_param_values(array_params) - array_params.each { |param| (param[1] || "".dup).gsub! %r/^"|"$/, "" } + array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" } end # This method takes an authorization body and splits up the key-value @@ -511,7 +511,7 @@ module ActionController # Returns nothing. def authentication_request(controller, realm, message = nil) message ||= "HTTP Token: Access denied.\n" - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}") controller.__send__ :render, plain: message, status: :unauthorized end end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 2f4c8fb83c..083b762f5a 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -86,7 +86,7 @@ module ActionController # Note: SSEs are not currently supported by IE. However, they are supported # by Chrome, Firefox, Opera, and Safari. class SSE - WHITELISTED_OPTIONS = %w( retry event id ) + PERMITTED_OPTIONS = %w( retry event id ) def initialize(stream, options = {}) @stream = stream @@ -111,13 +111,13 @@ module ActionController def perform_write(json, options) current_options = @options.merge(options).stringify_keys - WHITELISTED_OPTIONS.each do |option_name| + PERMITTED_OPTIONS.each do |option_name| if (option_value = current_options[option_name]) @stream.write "#{option_name}: #{option_value}\n" end end - message = json.gsub("\n".freeze, "\ndata: ".freeze) + message = json.gsub("\n", "\ndata: ") @stream.write "data: #{message}\n\n" end end @@ -280,33 +280,35 @@ module ActionController raise error if error end - # Spawn a new thread to serve up the controller in. This is to get - # around the fact that Rack isn't based around IOs and we need to use - # a thread to stream data from the response bodies. Nobody should call - # this method except in Rails internals. Seriously! - def new_controller_thread # :nodoc: - Thread.new { - t2 = Thread.current - t2.abort_on_exception = true - yield - } + def response_body=(body) + super + response.close if response end - def log_error(exception) - logger = ActionController::Base.logger - return unless logger + private - logger.fatal do - message = "\n#{exception.class} (#{exception.message}):\n".dup - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << exception.backtrace.join("\n ") - "#{message}\n\n" + # Spawn a new thread to serve up the controller in. This is to get + # around the fact that Rack isn't based around IOs and we need to use + # a thread to stream data from the response bodies. Nobody should call + # this method except in Rails internals. Seriously! + def new_controller_thread # :nodoc: + Thread.new { + t2 = Thread.current + t2.abort_on_exception = true + yield + } end - end - def response_body=(body) - super - response.close if response - end + def log_error(exception) + logger = ActionController::Base.logger + return unless logger + + logger.fatal do + message = +"\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + "#{message}\n\n" + end + end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 2b55b9347c..118da11990 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -11,7 +11,7 @@ module ActionController #:nodoc: # @people = Person.all # end # - # That action implicitly responds to all formats, but formats can also be whitelisted: + # That action implicitly responds to all formats, but formats can also be explicitly enumerated: # # def index # @people = Person.all @@ -105,7 +105,7 @@ module ActionController #:nodoc: # # Mime::Type.register "image/jpg", :jpg # - # Respond to also allows you to specify a common block for different formats by using +any+: + # +respond_to+ also allows you to specify a common block for different formats by using +any+: # # def index # @people = Person.all diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 4c2b5120eb..2804a06a58 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -105,7 +105,7 @@ module ActionController when String request.protocol + request.host_with_port + options when Proc - _compute_redirect_to_location request, options.call + _compute_redirect_to_location request, instance_eval(&options) else url_for(options) end.delete("\0\r\n") diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 6d181e6456..7d0a944381 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -40,7 +40,7 @@ module ActionController def render_to_string(*) result = super if result.respond_to?(:each) - string = "".dup + string = +"" result.each { |r| string << r } string else diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index edfef39771..cb109c6ad8 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -17,7 +17,7 @@ module ActionController #:nodoc: # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. All requests are checked except GET requests # as these should be idempotent. Keep in mind that all session-oriented requests - # should be CSRF protected, including JavaScript and HTML requests. + # are CSRF protected by default, including JavaScript and HTML requests. # # Since HTML and JavaScript requests are typically made from the browser, we # need to ensure to verify request authenticity for the web browser. We can @@ -30,16 +30,23 @@ module ActionController #:nodoc: # URL on your site. When your JavaScript response loads on their site, it executes. # With carefully crafted JavaScript on their end, sensitive data in your JavaScript # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or - # Ajax) requests are allowed to make GET requests for JavaScript responses. + # Ajax) requests are allowed to make requests for JavaScript responses. # - # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you should change forgery protection method in + # It's important to remember that XML or JSON requests are also checked by default. If + # you're building an API or an SPA you could change forgery protection method in # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base # protect_from_forgery unless: -> { request.format.json? } # end # + # It is generally safe to exclude XHR requests from CSRF protection + # (like the code snippet above does), because XHR requests can only be made from + # the same origin. Note however that any cross-origin third party domain + # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing] + # will also be able to create XHR requests. Be sure to check your + # CORS configuration before disabling forgery protection for XHR. + # # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. # By default <tt>protect_from_forgery</tt> protects your session with # <tt>:null_session</tt> method, which provides an empty session diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7af29f8dca..04922b0715 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -58,7 +58,7 @@ module ActionController # == Action Controller \Parameters # - # Allows you to choose which attributes should be whitelisted for mass updating + # Allows you to choose which attributes should be permitted for mass updating # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter @@ -133,6 +133,15 @@ module ActionController # Returns a hash that can be used as the JSON representation for the parameters. ## + # :method: each_key + # + # :call-seq: + # each_key() + # + # Calls block once for each key in the parameters, passing the key. + # If no block is given, an enumerator is returned instead. + + ## # :method: empty? # # :call-seq: @@ -204,7 +213,7 @@ module ActionController # # Returns a new array of the values of the parameters. delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, - :as_json, :to_s, to: :@parameters + :as_json, :to_s, :each_key, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -339,6 +348,14 @@ module ActionController end alias_method :each, :each_pair + # Convert all hashes in values into parameters, then yield each value in + # the same way as <tt>Hash#each_value</tt>. + def each_value(&block) + @parameters.each_pair do |key, value| + yield convert_hashes_to_parameters(key, value) + end + end + # Attribute that keeps track of converted arrays, if any, to avoid double # looping in the common use case permit + mass-assignment. Defined in a # method to instantiate it only if needed. @@ -505,7 +522,7 @@ module ActionController # # Note that if you use +permit+ in a key that points to a hash, # it won't allow all the hash. You also need to specify which - # attributes inside the hash should be whitelisted. + # attributes inside the hash should be permitted. # # params = ActionController::Parameters.new({ # person: { @@ -904,15 +921,28 @@ module ActionController PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } end - def permitted_scalar_filter(params, key) - if has_key?(key) && permitted_scalar?(self[key]) - params[key] = self[key] + # Adds existing keys to the params if their values are scalar. + # + # For example: + # + # puts self.keys #=> ["zipcode(90210i)"] + # params = {} + # + # permitted_scalar_filter(params, "zipcode") + # + # puts params.keys # => ["zipcode"] + def permitted_scalar_filter(params, permitted_key) + permitted_key = permitted_key.to_s + + if has_key?(permitted_key) && permitted_scalar?(self[permitted_key]) + params[permitted_key] = self[permitted_key] end - keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k| - if permitted_scalar?(self[k]) - params[k] = self[k] - end + each_key do |key| + next unless key =~ /\(\d+[if]?\)\z/ + next unless $~.pre_match == permitted_key + + params[key] = self[key] if permitted_scalar?(self[key]) end end @@ -997,8 +1027,8 @@ module ActionController # # It provides an interface for protecting attributes from end-user # assignment. This makes Action Controller parameters forbidden - # to be used in Active Model mass assignment until they have been - # whitelisted. + # to be used in Active Model mass assignment until they have been explicitly + # enumerated. # # In addition, parameters can be marked as required and flow through a # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no @@ -1034,7 +1064,7 @@ module ActionController # end # # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you - # will need to specify which nested attributes should be whitelisted. You might want + # will need to specify which nested attributes should be permitted. You might want # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information. # # class Person @@ -1052,7 +1082,7 @@ module ActionController # private # # def person_params - # # It's mandatory to specify the nested attributes that should be whitelisted. + # # It's mandatory to specify the nested attributes that should be permitted. # # If you use `permit` with just the key that points to the nested attributes hash, # # it will return an empty hash. # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ]) diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 84dbb59a63..f077e765ab 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -44,7 +44,7 @@ module ActionController options[:original_script_name] = original_script_name else if same_origin - options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup + options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup else options[:script_name] = script_name end diff --git a/actionpack/lib/action_controller/railties/helpers.rb b/actionpack/lib/action_controller/railties/helpers.rb index fa746fa9e8..75938108d6 100644 --- a/actionpack/lib/action_controller/railties/helpers.rb +++ b/actionpack/lib/action_controller/railties/helpers.rb @@ -7,7 +7,7 @@ module ActionController super return unless klass.respond_to?(:helpers_path=) - if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_helpers_paths) } + if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_helpers_paths) } paths = namespace.railtie_helpers_paths else paths = ActionController::Helpers.helpers_path diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index 2d1523f0fc..2b4559c760 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -81,7 +81,7 @@ module ActionController # * <tt>:html</tt> - Renders the provided HTML safe string, otherwise # performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>. # * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't - # need to call <tt>.to_json<tt> on the object you want to render. + # need to call <tt>.to_json</tt> on the object you want to render. # * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>. # # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, the default is diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index a7c7cfc1e5..f67b13f657 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -4,8 +4,8 @@ module ActionDispatch module Http module Cache module Request - HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze - HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze + HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE" + HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH" def if_modified_since if since = get_header(HTTP_IF_MODIFIED_SINCE) @@ -124,8 +124,8 @@ module ActionDispatch private - DATE = "Date".freeze - LAST_MODIFIED = "Last-Modified".freeze + DATE = "Date" + LAST_MODIFIED = "Last-Modified" SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate]) def generate_weak_etag(validators) @@ -166,11 +166,11 @@ module ActionDispatch @cache_control = cache_control_headers end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze - NO_CACHE = "no-cache".freeze - PUBLIC = "public".freeze - PRIVATE = "private".freeze - MUST_REVALIDATE = "must-revalidate".freeze + DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" + NO_CACHE = "no-cache" + PUBLIC = "public" + PRIVATE = "private" + MUST_REVALIDATE = "must-revalidate" def handle_conditional_get! # Normally default cache control setting is handled by ETag diff --git a/actionpack/lib/action_dispatch/http/content_disposition.rb b/actionpack/lib/action_dispatch/http/content_disposition.rb new file mode 100644 index 0000000000..58164c1522 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/content_disposition.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ActionDispatch + module Http + class ContentDisposition # :nodoc: + def self.format(disposition:, filename:) + new(disposition: disposition, filename: filename).to_s + end + + attr_reader :disposition, :filename + + def initialize(disposition:, filename:) + @disposition = disposition + @filename = filename + end + + TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/ + + def ascii_filename + 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"' + end + + RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/ + + def utf8_filename + "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR) + end + + def to_s + if filename + "#{disposition}; #{ascii_filename}; #{utf8_filename}" + else + "#{disposition}" + end + end + + private + def percent_escape(string, pattern) + string.gsub(pattern) do |char| + char.bytes.map { |byte| "%%%02X" % byte }.join + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index 35041fd072..b1e5a28be5 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -5,9 +5,9 @@ require "active_support/core_ext/object/deep_dup" module ActionDispatch #:nodoc: class ContentSecurityPolicy class Middleware - CONTENT_TYPE = "Content-Type".freeze - POLICY = "Content-Security-Policy".freeze - POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze + CONTENT_TYPE = "Content-Type" + POLICY = "Content-Security-Policy" + POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only" def initialize(app) @app = app @@ -22,7 +22,8 @@ module ActionDispatch #:nodoc: if policy = request.content_security_policy nonce = request.content_security_policy_nonce - headers[header_name(request)] = policy.build(request.controller_instance, nonce) + context = request.controller_instance || request + headers[header_name(request)] = policy.build(context, nonce) end response @@ -50,10 +51,10 @@ module ActionDispatch #:nodoc: end module Request - POLICY = "action_dispatch.content_security_policy".freeze - POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze - NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze - NONCE = "action_dispatch.content_security_policy_nonce".freeze + POLICY = "action_dispatch.content_security_policy" + POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only" + NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator" + NONCE = "action_dispatch.content_security_policy_nonce" def content_security_policy get_header(POLICY) @@ -132,7 +133,7 @@ module ActionDispatch #:nodoc: worker_src: "worker-src" }.freeze - NONCE_DIRECTIVES = %w[script-src].freeze + NONCE_DIRECTIVES = %w[script-src style-src].freeze private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES @@ -257,7 +258,8 @@ module ActionDispatch #:nodoc: if context.nil? raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}" else - context.instance_exec(&source) + resolved = context.instance_exec(&source) + resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved end else raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index ec012ad02d..9f8f178402 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "action_dispatch/http/parameter_filter" +require "active_support/parameter_filter" module ActionDispatch module Http @@ -28,8 +28,8 @@ module ActionDispatch # => reverses the value to all keys matching /secret/i module FilterParameters ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc: - NULL_PARAM_FILTER = ParameterFilter.new # :nodoc: - NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc: + NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc: + NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc: def initialize super @@ -69,7 +69,7 @@ module ActionDispatch end def parameter_filter_for(filters) # :doc: - ParameterFilter.new(filters) + ActiveSupport::ParameterFilter.new(filters) end KV_RE = "[^&;=]+" diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 25394fe5dd..8c4e852235 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -3,7 +3,7 @@ module ActionDispatch module Http module FilterRedirect - FILTERED = "[FILTERED]".freeze # :nodoc: + FILTERED = "[FILTERED]" # :nodoc: def filtered_location # :nodoc: if location_filter_match? diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index d7435fa8df..be129965d1 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -85,10 +85,7 @@ module ActionDispatch if variant.all? { |v| v.is_a?(Symbol) } @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ - "For security reasons, never directly set the variant to a user-provided value, " \ - "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ - "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols." end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 295539281f..c3e0ea3c89 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# -*- frozen-string-literal: true -*- - require "singleton" require "active_support/core_ext/string/starts_ends_with" @@ -74,7 +72,7 @@ module Mime def initialize(index, name, q = nil) @index = index @name = name - q ||= 0.0 if @name == "*/*".freeze # Default wildcard match to end of list. + q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list. @q = ((q || 1.0).to_f * 100).to_i end diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 1d58964862..ddeb3d81e2 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -1,86 +1,12 @@ # frozen_string_literal: true -require "active_support/core_ext/object/duplicable" +require "active_support/deprecation/constant_accessor" +require "active_support/parameter_filter" module ActionDispatch module Http - class ParameterFilter - FILTERED = "[FILTERED]".freeze # :nodoc: - - def initialize(filters = []) - @filters = filters - end - - def filter(params) - compiled_filter.call(params) - end - - private - - def compiled_filter - @compiled_filter ||= CompiledFilter.compile(@filters) - end - - class CompiledFilter # :nodoc: - def self.compile(filters) - return lambda { |params| params.dup } if filters.empty? - - strings, regexps, blocks = [], [], [] - - filters.each do |item| - case item - when Proc - blocks << item - when Regexp - regexps << item - else - strings << Regexp.escape(item.to_s) - end - end - - deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } - deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } - - regexps << Regexp.new(strings.join("|".freeze), true) unless strings.empty? - deep_regexps << Regexp.new(deep_strings.join("|".freeze), true) unless deep_strings.empty? - - new regexps, deep_regexps, blocks - end - - attr_reader :regexps, :deep_regexps, :blocks - - def initialize(regexps, deep_regexps, blocks) - @regexps = regexps - @deep_regexps = deep_regexps.any? ? deep_regexps : nil - @blocks = blocks - end - - def call(original_params, parents = []) - filtered_params = original_params.class.new - - original_params.each do |key, value| - parents.push(key) if deep_regexps - if regexps.any? { |r| key =~ r } - value = FILTERED - elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r } - value = FILTERED - elsif value.is_a?(Hash) - value = call(value, parents) - elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } - elsif blocks.any? - key = key.dup if key.duplicable? - value = value.dup if value.duplicable? - blocks.each { |b| b.call(key, value) } - end - parents.pop if deep_regexps - - filtered_params[key] = value - end - - filtered_params - end - end - end + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + deprecate_constant "ParameterFilter", "ActiveSupport::ParameterFilter", + message: "ActionDispatch::Http::ParameterFilter is deprecated and will be removed from Rails 6.1. Use ActiveSupport::ParameterFilter instead." end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 3838b84a7a..7bc364d370 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -136,11 +136,11 @@ module ActionDispatch end def routes # :nodoc: - get_header("action_dispatch.routes".freeze) + get_header("action_dispatch.routes") end def routes=(routes) # :nodoc: - set_header("action_dispatch.routes".freeze, routes) + set_header("action_dispatch.routes", routes) end def engine_script_name(_routes) # :nodoc: @@ -158,11 +158,11 @@ module ActionDispatch end def controller_instance # :nodoc: - get_header("action_controller.instance".freeze) + get_header("action_controller.instance") end def controller_instance=(controller) # :nodoc: - set_header("action_controller.instance".freeze, controller) + set_header("action_controller.instance", controller) end def http_auth_salt @@ -173,7 +173,7 @@ module ActionDispatch # We're treating `nil` as "unset", and we want the default setting to be # `true`. This logic should be extracted to `env_config` and calculated # once. - !(get_header("action_dispatch.show_exceptions".freeze) == false) + !(get_header("action_dispatch.show_exceptions") == false) end # Returns a symbol form of the #request_method. @@ -280,10 +280,10 @@ module ActionDispatch end def remote_ip=(remote_ip) - set_header "action_dispatch.remote_ip".freeze, remote_ip + set_header "action_dispatch.remote_ip", remote_ip end - ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc: # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware @@ -407,18 +407,18 @@ module ActionDispatch def request_parameters=(params) raise if params.nil? - set_header("action_dispatch.request.request_parameters".freeze, params) + set_header("action_dispatch.request.request_parameters", params) end def logger - get_header("action_dispatch.logger".freeze) + get_header("action_dispatch.logger") end def commit_flash end def ssl? - super || scheme == "wss".freeze + super || scheme == "wss" end private diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 7e50cb6d23..1d38942a31 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -78,9 +78,9 @@ module ActionDispatch # :nodoc: x end - CONTENT_TYPE = "Content-Type".freeze - SET_COOKIE = "Set-Cookie".freeze - LOCATION = "Location".freeze + CONTENT_TYPE = "Content-Type" + SET_COOKIE = "Set-Cookie" + LOCATION = "Location" NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] cattr_accessor :default_charset, default: "utf-8" @@ -105,7 +105,7 @@ module ActionDispatch # :nodoc: def body @str_body ||= begin - buf = "".dup + buf = +"" each { |chunk| buf << chunk } buf end @@ -224,16 +224,6 @@ module ActionDispatch # :nodoc: @status = Rack::Utils.status_code(status) end - # Sets the HTTP content type. - def content_type=(content_type) - return unless content_type - new_header_info = parse_content_type(content_type.to_s) - prev_header_info = parsed_content_type_header - charset = new_header_info.charset || prev_header_info.charset - charset ||= self.class.default_charset unless prev_header_info.mime_type - set_content_type new_header_info.mime_type, charset - end - # Sets the HTTP response's content MIME type. For example, in the controller # you could write this: # @@ -242,7 +232,17 @@ module ActionDispatch # :nodoc: # If a character set has been defined for this response (see charset=) then # the character set information will also be included in the content type # information. + def content_type=(content_type) + return unless content_type + new_header_info = parse_content_type(content_type.to_s) + prev_header_info = parsed_content_type_header + charset = new_header_info.charset || prev_header_info.charset + charset ||= self.class.default_charset unless prev_header_info.mime_type + set_content_type new_header_info.mime_type, charset + end + # Content type of response. + # It returns just MIME type and does NOT contain charset part. def content_type parsed_content_type_header.mime_type end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 35ba44005a..3af4c176a7 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -67,7 +67,7 @@ module ActionDispatch end def path_for(options) - path = options[:script_name].to_s.chomp("/".freeze) + path = options[:script_name].to_s.chomp("/") path << options[:path] if options.key?(:path) add_trailing_slash(path) if options[:trailing_slash] @@ -157,7 +157,7 @@ module ActionDispatch subdomain = options.fetch :subdomain, true domain = options[:domain] - host = "".dup + host = +"" if subdomain == true return _host if domain.nil? @@ -231,7 +231,7 @@ module ActionDispatch # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host - raw_host_with_port.sub(/:\d+$/, "".freeze) + raw_host_with_port.sub(/:\d+$/, "") end # Returns a \host:\port string for this request, such as "example.com" or diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 0f04839d9b..52396ec901 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -50,7 +50,7 @@ module ActionDispatch unmatched_keys = (missing_keys || []) & constraints.keys missing_keys = (missing_keys || []) - unmatched_keys - message = "No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}".dup + message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}" message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty? diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index df3f79a407..3c8b9a6eaa 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -17,11 +17,11 @@ module ActionDispatch def self.normalize_path(path) path ||= "" encoding = path.encoding - path = "/#{path}".dup - path.squeeze!("/".freeze) - path.sub!(%r{/+\Z}, "".freeze) + path = +"/#{path}" + path.squeeze!("/") + path.sub!(%r{/+\Z}, "") path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } - path = "/".dup if path == "".freeze + path = +"/" if path == "" path.force_encoding(encoding) path end @@ -29,16 +29,16 @@ module ActionDispatch # URI path and fragment escaping # https://tools.ietf.org/html/rfc3986 class UriEncoder # :nodoc: - ENCODE = "%%%02X".freeze + ENCODE = "%%%02X" US_ASCII = Encoding::US_ASCII UTF_8 = Encoding::UTF_8 - EMPTY = "".dup.force_encoding(US_ASCII).freeze + EMPTY = (+"").force_encoding(US_ASCII).freeze DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) } - ALPHA = "a-zA-Z".freeze - DIGIT = "0-9".freeze - UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze - SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze + ALPHA = "a-zA-Z" + DIGIT = "0-9" + UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~" + SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=" ESCAPED = /%[a-zA-Z0-9]{2}/.freeze diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 3395471a85..d2619cbf3a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -40,7 +40,7 @@ module ActionDispatch @parameters.each do |index| param = parts[index] value = hash[param.name] - return "".freeze unless value + return "" unless value parts[index] = param.escape value end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index c45d947904..26d3fd936f 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -9,7 +9,7 @@ require "rack/utils" module ActionDispatch class Request def cookie_jar - fetch_header("action_dispatch.cookies".freeze) do + fetch_header("action_dispatch.cookies") do self.cookie_jar = Cookies::CookieJar.build(self, cookies) end end @@ -22,11 +22,11 @@ module ActionDispatch } def have_cookie_jar? - has_header? "action_dispatch.cookies".freeze + has_header? "action_dispatch.cookies" end def cookie_jar=(jar) - set_header "action_dispatch.cookies".freeze, jar + set_header "action_dispatch.cookies", jar end def key_generator @@ -81,6 +81,10 @@ module ActionDispatch get_header Cookies::COOKIES_ROTATIONS end + def use_cookies_with_metadata + get_header Cookies::USE_COOKIES_WITH_METADATA + end + # :startdoc: end @@ -168,20 +172,21 @@ module ActionDispatch # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. class Cookies - HTTP_HEADER = "Set-Cookie".freeze - GENERATOR_KEY = "action_dispatch.key_generator".freeze - SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze - ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze - ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze - AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze - USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze - ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze - SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze - SECRET_TOKEN = "action_dispatch.secret_token".freeze - SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze - COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze - COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze - COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze + HTTP_HEADER = "Set-Cookie" + GENERATOR_KEY = "action_dispatch.key_generator" + SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt" + ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt" + ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt" + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt" + USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption" + ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher" + SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest" + SECRET_TOKEN = "action_dispatch.secret_token" + SECRET_KEY_BASE = "action_dispatch.secret_key_base" + COOKIES_SERIALIZER = "action_dispatch.cookies_serializer" + COOKIES_DIGEST = "action_dispatch.cookies_digest" + COOKIES_ROTATIONS = "action_dispatch.cookies_rotations" + USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata" # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -470,7 +475,7 @@ module ActionDispatch def [](name) if data = @parent_jar[name.to_s] - parse name, data + parse(name, data, purpose: "cookie.#{name}") || parse(name, data) end end @@ -481,7 +486,7 @@ module ActionDispatch options = { value: options } end - commit(options) + commit(name, options) @parent_jar[name] = options end @@ -497,13 +502,24 @@ module ActionDispatch end end - def parse(name, data); data; end - def commit(options); end + def cookie_metadata(name, options) + if request.use_cookies_with_metadata + metadata = expiry_options(options) + metadata[:purpose] = "cookie.#{name}" + + metadata + else + {} + end + end + + def parse(name, data, purpose: nil); data; end + def commit(name, options); end end class PermanentCookieJar < AbstractCookieJar # :nodoc: private - def commit(options) + def commit(name, options) options[:expires] = 20.years.from_now end end @@ -519,7 +535,7 @@ module ActionDispatch end module SerializedCookieJars # :nodoc: - MARSHAL_SIGNATURE = "\x04\x08".freeze + MARSHAL_SIGNATURE = "\x04\x08" SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer protected @@ -583,14 +599,14 @@ module ActionDispatch end private - def parse(name, signed_message) + def parse(name, signed_message, purpose: nil) deserialize(name) do |rotate| - @verifier.verified(signed_message, on_rotation: rotate) + @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose) end end - def commit(options) - options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options)) + def commit(name, options) + options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options)) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end @@ -631,16 +647,16 @@ module ActionDispatch end private - def parse(name, encrypted_message) + def parse(name, encrypted_message, purpose: nil) deserialize(name) do |rotate| - @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate) + @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose) end rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature parse_legacy_signed_message(name, encrypted_message) end - def commit(options) - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options)) + def commit(name, options) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options)) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 077a83b112..5f5fdbc66a 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -23,7 +23,7 @@ module ActionDispatch if clean_params.empty? "None" else - PP.pp(clean_params, "".dup, 200) + PP.pp(clean_params, +"", 200) end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_locks.rb b/actionpack/lib/action_dispatch/middleware/debug_locks.rb index 03760438f7..93c6c85a71 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_locks.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_locks.rb @@ -32,7 +32,7 @@ module ActionDispatch req = ActionDispatch::Request.new env if req.get? - path = req.path_info.chomp("/".freeze) + path = req.path_info.chomp("/") if path == @path return render_details(req) end @@ -63,19 +63,19 @@ module ActionDispatch str = threads.map do |thread, info| if info[:exclusive] - lock_state = "Exclusive".dup + lock_state = +"Exclusive" elsif info[:sharing] > 0 - lock_state = "Sharing".dup + lock_state = +"Sharing" lock_state << " x#{info[:sharing]}" if info[:sharing] > 1 else - lock_state = "No lock".dup + lock_state = +"No lock" end if info[:waiting] lock_state << " (yielded share)" end - msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup + msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n" if info[:sleeper] msg << " Waiting in #{info[:sleeper]}" diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index fd05eec172..cf9165d008 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -38,7 +38,7 @@ module ActionDispatch # # See docs on the FlashHash class for more details about the flash. class Flash - KEY = "action_dispatch.request.flash_hash".freeze + KEY = "action_dispatch.request.flash_hash" module RequestMethods # Access the contents of the flash. Use <tt>flash["notice"]</tt> to diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index da2871b551..fcc0c72240 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -15,7 +15,7 @@ module ActionDispatch # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files # from multiple pieces of the stack. class RequestId - X_REQUEST_ID = "X-Request-Id".freeze #:nodoc: + X_REQUEST_ID = "X-Request-Id" #:nodoc: def initialize(app) @app = app @@ -30,7 +30,7 @@ module ActionDispatch private def make_request_id(request_id) if request_id.presence - request_id.gsub(/[^\w\-@]/, "".freeze).first(255) + request_id.gsub(/[^\w\-@]/, "").first(255) else internal_request_id end diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 190e54223e..00902ede21 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -83,7 +83,7 @@ module ActionDispatch private def set_hsts_header!(headers) - headers["Strict-Transport-Security".freeze] ||= @hsts_header + headers["Strict-Transport-Security"] ||= @hsts_header end def normalize_hsts_options(options) @@ -102,23 +102,23 @@ module ActionDispatch # https://tools.ietf.org/html/rfc6797#section-6.1 def build_hsts_header(hsts) - value = "max-age=#{hsts[:expires].to_i}".dup + value = +"max-age=#{hsts[:expires].to_i}" value << "; includeSubDomains" if hsts[:subdomains] value << "; preload" if hsts[:preload] value end def flag_cookies_as_secure!(headers) - if cookies = headers["Set-Cookie".freeze] - cookies = cookies.split("\n".freeze) + if cookies = headers["Set-Cookie"] + cookies = cookies.split("\n") - headers["Set-Cookie".freeze] = cookies.map { |cookie| + headers["Set-Cookie"] = cookies.map { |cookie| if !/;\s*secure\s*(;|$)/i.match?(cookie) "#{cookie}; secure" else cookie end - }.join("\n".freeze) + }.join("\n") end end @@ -141,7 +141,7 @@ module ActionDispatch host = @redirect[:host] || request.host port = @redirect[:port] || request.port - location = "https://#{host}".dup + location = +"https://#{host}" location << ":#{port}" if port != 80 && port != 443 location << request.fullpath location diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 277074f216..1f2f7757a3 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -79,7 +79,7 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze) + ::Rack::Mime.mime_type(::File.extname(path), "text/plain") end def gzip_encoding_accepted?(request) @@ -116,7 +116,7 @@ module ActionDispatch req = Rack::Request.new env if req.get? || req.head? - path = req.path_info.chomp("/".freeze) + path = req.path_info.chomp("/") if match = @file_handler.match?(path) req.path_info = match return @file_handler.serve(req) diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 1fa0691303..0242b706b2 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -197,4 +197,7 @@ setupMatchPaths(); setupRouteToggleHelperLinks(); + + // Focus the search input after page has loaded + document.getElementById('search').focus(); </script> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index eb6fbca6ba..efc3988bc3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -21,6 +21,7 @@ module ActionDispatch config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" config.action_dispatch.use_authenticated_cookie_encryption = false + config.action_dispatch.use_cookies_with_metadata = false config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 0ae464082d..fb0efb9a58 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/hash/indifferent_access" + module ActionDispatch class Request class Utils # :nodoc: diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index cba49d1a0b..413e524ef6 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -83,7 +83,7 @@ module ActionDispatch private def normalize_filter(filter) if filter[:controller] - { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } + { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ } elsif filter[:grep] { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/, verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ } diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ff325afc54..99f3b4c2cd 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -50,7 +50,19 @@ module ActionDispatch private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.path_parameters, request] + arity = if constraint.respond_to?(:arity) + constraint.arity + else + constraint.method(:call).arity + end + + if arity < 1 + [] + elsif arity == 1 + [request] + else + [request.path_parameters, request] + end end end @@ -308,7 +320,7 @@ module ActionDispatch def check_controller_and_action(path_params, controller, action) hash = check_part(:controller, controller, path_params, {}) do |part| translate_controller(part) { - message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup + message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems." message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" raise ArgumentError, message @@ -390,7 +402,7 @@ module ActionDispatch # for root cases, where the latter is the correct one. def self.normalize_path(path) path = Journey::Router::Utils.normalize_path(path) - path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$} path end @@ -553,10 +565,10 @@ module ActionDispatch # # match 'json_only', constraints: { format: 'json' }, via: :get # - # class Whitelist + # class PermitList # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get + # match 'path', to: 'c#a', constraints: PermitList.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -668,7 +680,7 @@ module ActionDispatch script_namer = ->(options) do prefix_options = options.slice(*_route.segment_keys) - prefix_options[:relative_url_root] = "".freeze + prefix_options[:relative_url_root] = "" if options[:_recall] prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys)) diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index e17ccaf986..4de5f9e2f7 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -181,8 +181,8 @@ module ActionDispatch CACHE[type].fetch(action) { build action, type } end - def self.url; CACHE["url".freeze][nil]; end - def self.path; CACHE["path".freeze][nil]; end + def self.url; CACHE["url"][nil]; end + def self.path; CACHE["path"][nil]; end def self.build(action, type) prefix = action ? "#{action}_" : "" diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 07d3a41173..2ae75b0da8 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -245,7 +245,7 @@ module ActionDispatch missing_keys << missing_key } constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }] - message = "No route matches #{constraints.inspect}".dup + message = +"No route matches #{constraints.inspect}" message << ", missing required keys: #{missing_keys.sort.inspect}" raise ActionController::UrlGenerationError, message @@ -377,7 +377,7 @@ module ActionDispatch @prepend = [] @disable_clear_and_finalize = false @finalized = false - @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze + @env_key = "ROUTES_#{object_id}_SCRIPT_NAME" @set = Journey::Routes.new @router = Journey::Router.new @set @@ -729,7 +729,7 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! if controller - if controller.start_with?("/".freeze) + if controller.start_with?("/") @options[:controller] = controller[1..-1] else @options[:controller] = controller @@ -820,10 +820,6 @@ module ActionDispatch path, params = generate(route_name, path_options, recall) - if options.key? :params - params.merge! options[:params] - end - options[:path] = path options[:script_name] = script_name options[:params] = params diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb index d2685e0452..884fb51d18 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -65,7 +65,7 @@ module ActionDispatch end def display_image - message = "[Screenshot]: #{image_path}\n".dup + message = +"[Screenshot]: #{image_path}\n" case output_type when "artifact" diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 98b1965d22..8595ea03cf 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -79,9 +79,8 @@ module ActionDispatch end def generate_response_message(expected, actual = @response.response_code) - "Expected response to be a <#{code_with_name(expected)}>,"\ - " but was a <#{code_with_name(actual)}>" - .dup.concat(location_if_redirected).concat(response_body_if_short) + (+"Expected response to be a <#{code_with_name(expected)}>,"\ + " but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short) end def response_body_if_short diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 77cb311630..af41521c5c 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -9,6 +9,11 @@ module ActionDispatch module Assertions # Suite of assertions to test routes generated by \Rails and the handling of requests made to them. module RoutingAssertions + def setup # :nodoc: + @routes ||= nil + super + end + # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+. # diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 8ac50c730d..0b98f27f11 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -8,12 +8,12 @@ module ActionDispatch module FixtureFile # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: # - # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') + # post :change_avatar, params: { avatar: fixture_file_upload('files/spongebob.png', 'image/png') } # # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter. # This will not affect other platforms: # - # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary) + # post :change_avatar, params: { avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary) } def fixture_file_upload(path, mime_type = nil, binary = false) if self.class.respond_to?(:fixture_path) && self.class.fixture_path && !File.exist?(path) diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index f4787ed27a..65dd28b3d7 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -232,6 +232,7 @@ module ActionController routes = ActionDispatch::Routing::RouteSet.new routes.draw(&block) include routes.url_helpers + routes end end @@ -430,14 +431,16 @@ end class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end class DrivenByRackTest < ActionDispatch::SystemTestCase diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index a672ede1a9..558e710df9 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -138,7 +138,7 @@ class ControllerInstanceTests < ActiveSupport::TestCase response_headers = SimpleController.action("hello").call( "REQUEST_METHOD" => "GET", - "rack.input" => -> {} + "rack.input" => -> { } )[1] assert response_headers.key?("X-Frame-Options") @@ -193,7 +193,7 @@ class UrlOptionsTest < ActionController::TestCase action: "home", controller: "pages", only_path: true, - params: { "token" => "secret" } + token: "secret" } assert_equal "/home?token=secret", rs.url_for(options) diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 34bc2c0caa..409a4ec2e6 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -342,6 +342,21 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end + def test_flash_usable_in_metal_without_helper + controller_class = nil + + assert_nothing_raised do + controller_class = Class.new(ActionController::Metal) do + include ActionController::Flash + end + end + + controller = controller_class.new + + assert_respond_to controller, :alert + assert_respond_to controller, :notice + end + private # Overwrite get to send SessionSecret in env hash diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 672aa1351c..103123f98c 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -150,7 +150,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase end test "token_and_options returns empty string with empty token" do - token = "".dup + token = +"" actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first expected = token assert_equal(expected, actual) diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 431fe90b23..d81c43b87d 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -304,7 +304,7 @@ module ActionController # Simulate InterlockHook ActiveSupport::Dependencies.interlock.start_running res = get :write_sleep_autoload - res.each {} + res.each { } ActiveSupport::Dependencies.interlock.done_running end diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb index 248ef36b7c..7b53092266 100644 --- a/actionpack/test/controller/metal_test.rb +++ b/actionpack/test/controller/metal_test.rb @@ -20,7 +20,7 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase response_headers = SimpleController.action("hello").call( "REQUEST_METHOD" => "GET", - "rack.input" => -> {} + "rack.input" => -> { } )[1] assert_not response_headers.key?("X-Frame-Options") diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 1163775d3c..00e1d5f3b3 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -78,7 +78,7 @@ class RespondToController < ActionController::Base def missing_templates respond_to do |type| # This test requires a block that is empty - type.json {} + type.json { } type.xml end end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index b049022a06..7572d514fb 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -13,7 +13,7 @@ module BareMetalTest test "response body is a Rack-compatible response" do status, headers, body = BareController.action(:index).call(Rack::MockRequest.env_for("/")) assert_equal 200, status - string = "".dup + string = +"" body.each do |part| assert part.is_a?(String), "Each part of the body must be a String" diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb index 07fbadae9f..5e570a1d79 100644 --- a/actionpack/test/controller/new_base/render_context_test.rb +++ b/actionpack/test/controller/new_base/render_context_test.rb @@ -32,10 +32,11 @@ module RenderContext "controller context!" end - # 3) Set view_context to self - private def view_context - self - end + private + # 3) Set view_context to self + def view_context + self + end end class RenderContextTest < Rack::TestCase diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 68c7f2d9ea..7789e654d5 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -75,6 +75,28 @@ class ParametersAccessorsTest < ActiveSupport::TestCase end end + test "each_value carries permitted status" do + @params.permit! + @params.each_value do |value| + assert_predicate(value, :permitted?) + end + end + + test "each_value carries unpermitted status" do + @params.each_value do |value| + assert_not_predicate(value, :permitted?) + end + end + + test "each_key converts to hash for permitted" do + @params.permit! + @params.each_key { |key| assert_kind_of(String, key) if key == "person" } + end + + test "each_key converts to hash for unpermitted" do + @params.each_key { |key| assert_kind_of(String, key) if key == "person" } + end + test "empty? returns true when params contains no key/value pairs" do params = ActionController::Parameters.new assert_empty params diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index fe0e5e368d..974612fb7b 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -20,7 +20,7 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase end end - test "permits parameters that are whitelisted" do + test "allows both explicitly listed and always-permitted parameters" do params = ActionController::Parameters.new( book: { pages: 65 }, format: "json") diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 2959dc3e4d..998498e1b2 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -5,6 +5,12 @@ require "abstract_unit" class Workshop extend ActiveModel::Naming include ActiveModel::Conversion + + OUT_OF_SCOPE_BLOCK = proc do + raise "Not executed in controller's context" unless RedirectController === self + request.original_url + end + attr_accessor :id def initialize(id) @@ -119,6 +125,10 @@ class RedirectController < ActionController::Base redirect_to proc { { action: "hello_world" } } end + def redirect_to_out_of_scope_block + redirect_to Workshop::OUT_OF_SCOPE_BLOCK + end + def redirect_with_header_break redirect_to "/lol\r\nwat" end @@ -204,6 +214,13 @@ class RedirectTest < ActionController::TestCase assert_equal "http://test.host/things/stuff", redirect_to_url end + def test_relative_url_redirect_host_with_port + request.host = "test.host:1234" + get :relative_url_redirect_with_status + assert_response 302 + assert_equal "http://test.host:1234/things/stuff", redirect_to_url + end + def test_simple_redirect_using_options get :host_redirect assert_response :redirect @@ -326,6 +343,12 @@ class RedirectTest < ActionController::TestCase assert_redirected_to "http://www.rubyonrails.org/" end + def test_redirect_to_out_of_scope_block + get :redirect_to_out_of_scope_block + assert_response :redirect + assert_redirected_to "http://test.host/redirect/redirect_to_out_of_scope_block" + end + def test_redirect_to_with_block_and_accepted_options with_routing do |set| set.draw do diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index a7033b2d30..30f2a23b33 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -25,8 +25,8 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ]) hex = unsafe.map { |char| "%" + char.unpack1("H2").upcase } - @segment = "#{safe.join}#{unsafe.join}".freeze - @escaped = "#{safe.join}#{hex.join}".freeze + @segment = "#{safe.join}#{unsafe.join}" + @escaped = "#{safe.join}#{hex.join}" end def test_route_generation_escapes_unsafe_path_characters @@ -309,7 +309,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_specific_controller_action_failure rs.draw do - mount lambda {} => "/foo" + mount lambda { } => "/foo" end assert_raises(ActionController::UrlGenerationError) do @@ -674,7 +674,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal "/page/foo", url_for(rs, controller: "content", action: "show_page", id: "foo") assert_equal({ controller: "content", action: "show_page", id: "foo" }, rs.recognize_path("/page/foo")) - token = "\321\202\320\265\320\272\321\201\321\202".dup # 'text' in Russian + token = +"\321\202\320\265\320\272\321\201\321\202" # 'text' in Russian token.force_encoding(Encoding::BINARY) escaped_token = CGI.escape(token) diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 7b1a52b277..c917cdf761 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -144,7 +144,7 @@ class SendFileTest < ActionController::TestCase get :test_send_file_headers_bang assert_equal "image/png", response.content_type - assert_equal 'disposition; filename="filename"', response.get_header("Content-Disposition") + assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition") assert_equal "binary", response.get_header("Content-Transfer-Encoding") assert_equal "private", response.get_header("Cache-Control") end @@ -153,7 +153,7 @@ class SendFileTest < ActionController::TestCase def test_send_file_headers_with_disposition_as_a_symbol get :test_send_file_headers_with_disposition_as_a_symbol - assert_equal 'disposition; filename="filename"', response.get_header("Content-Disposition") + assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition") end def test_send_file_headers_with_mime_lookup_with_symbol diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index dda2686a9b..6fc70d6248 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -542,7 +542,7 @@ XML def test_params_passing_with_frozen_values assert_nothing_raised do get :test_params, params: { - frozen: "icy".freeze, frozens: ["icy".freeze].freeze, deepfreeze: { frozen: "icy".freeze }.freeze + frozen: -"icy", frozens: [-"icy"].freeze, deepfreeze: { frozen: -"icy" }.freeze } end parsed_params = ::JSON.parse(@response.body) diff --git a/actionpack/test/dispatch/content_disposition_test.rb b/actionpack/test/dispatch/content_disposition_test.rb new file mode 100644 index 0000000000..3f5959da6e --- /dev/null +++ b/actionpack/test/dispatch/content_disposition_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + class ContentDispositionTest < ActiveSupport::TestCase + test "encoding a Latin filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "racecar.jpg") + + assert_equal %(filename="racecar.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''racecar.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "encoding a Latin filename with accented characters" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "råcëçâr.jpg") + + assert_equal %(filename="racecar.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''r%C3%A5c%C3%AB%C3%A7%C3%A2r.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "encoding a non-Latin filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "автомобиль.jpg") + + assert_equal %(filename="%3F%3F%3F%3F%3F%3F%3F%3F%3F%3F.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "without filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: nil) + + assert_equal "inline", disposition.to_s + end + end +end diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index 4f9a4ff2bd..c8c885f35c 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -260,12 +260,13 @@ class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationT ROUTES.draw do scope module: "default_content_security_policy_integration_test" do get "/", to: "policy#index" + get "/redirect", to: redirect("/") end end POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| - p.default_src :self - p.script_src :https + p.default_src -> { :self } + p.script_src -> { :https } end class PolicyConfigMiddleware @@ -295,14 +296,19 @@ class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationT def test_adds_nonce_to_script_src_content_security_policy_only_once get "/" get "/" + assert_response :success + assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" + end + + def test_redirect_works_with_dynamic_sources + get "/redirect" + assert_response :redirect assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" end private def assert_policy(expected, report_only: false) - assert_response :success - if report_only expected_header = "Content-Security-Policy-Report-Only" unexpected_header = "Content-Security-Policy" @@ -339,6 +345,11 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest p.script_src :self end + content_security_policy only: :style_src do |p| + p.default_src false + p.style_src :self + end + content_security_policy(false, only: :no_policy) content_security_policy_report_only only: :report_only @@ -363,6 +374,10 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest head :ok end + def style_src + head :ok + end + def no_policy head :ok end @@ -381,6 +396,7 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest get "/conditional", to: "policy#conditional" get "/report-only", to: "policy#report_only" get "/script-src", to: "policy#script_src" + get "/style-src", to: "policy#style_src" get "/no-policy", to: "policy#no_policy" end end @@ -441,6 +457,11 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='" end + def test_adds_nonce_to_style_src_content_security_policy + get "/style-src" + assert_policy "style-src 'self' 'nonce-iyhD0Yc0W+c='" + end + def test_generates_no_content_security_policy get "/no-policy" diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index aba778fad6..6637c2cae9 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -289,6 +289,46 @@ class CookiesTest < ActionController::TestCase cookies[:user_name] = { value: "assain", expires: 2.hours } head :ok end + + def encrypted_discount_and_user_id_cookie + cookies.encrypted[:user_id] = { value: 50, expires: 1.hour } + cookies.encrypted[:discount_percentage] = 10 + + head :ok + end + + def signed_discount_and_user_id_cookie + cookies.signed[:user_id] = { value: 50, expires: 1.hour } + cookies.signed[:discount_percentage] = 10 + + head :ok + end + + def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on + # cookies.encrypted[:favorite] = { value: "5-2-Stable Chocolate Cookies", expires: 1000.years } + cookies[:favorite] = "KvH5lIHvX5vPQkLIK63r/NuIMwzWky8M0Zwk8SZ6DwUv8+srf36geR4nWq5KmhsZIYXA8NRdCZYIfxMKJsOFlz77Gf+Fq8vBBCWJTp95rx39A28TCUTJEyMhCNJO5eie7Skef76Qt5Jo/SCnIADAhzyGQkGBopKRcA==--qXZZFWGbCy6N8AGy--WswoH+xHrNh9MzSXDpB2fA==" + + head :ok + end + + def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off + cookies[:favorite] = "Wmg4amgvcVVvWGcwK3c4WjJEbTdRQUgrWXhBdDliUTR0cVNidXpmVTMrc2RjcitwUzVsWWEwZGtuVGtFUjJwNi0tcVhVMTFMOTQ1d0hIVE1FK0pJc05SQT09--8b2a55c375049a50f7a959b9d42b31ef0b2bb594" + + head :ok + end + + def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on + # cookies.signed[:favorite] = { value: "5-2-Stable Choco Chip Cookie", expires: 1000.years } + cookies[:favorite] = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaUUxTFRJdFUzUmhZbXhsSUVOb2IyTnZJRU5vYVhBZ1EyOXZhMmxsQmpvR1JWUT0iLCJleHAiOiIzMDE4LTA3LTExVDE2OjExOjI2Ljc1M1oiLCJwdXIiOm51bGx9fQ==--7df5d885b78b70a501d6e82140ae91b24060ac00" + + head :ok + end + + def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off + cookies[:favorite] = "BAhJIiE1LTItU3RhYmxlIENob2NvIENoaXAgQ29va2llBjoGRVQ=--50bbdbf8d64f5a3ec3e54878f54d4f55b6cb3aff" + + head :ok + end end tests TestController @@ -1274,6 +1314,8 @@ class CookiesTest < ActionController::TestCase end def test_signed_cookie_with_expires_set_relatively + request.env["action_dispatch.use_cookies_with_metadata"] = true + cookies.signed[:user_name] = { value: "assain", expires: 2.hours } travel 1.hour @@ -1284,6 +1326,8 @@ class CookiesTest < ActionController::TestCase end def test_encrypted_cookie_with_expires_set_relatively + request.env["action_dispatch.use_cookies_with_metadata"] = true + cookies.encrypted[:user_name] = { value: "assain", expires: 2.hours } travel 1.hour @@ -1300,6 +1344,124 @@ class CookiesTest < ActionController::TestCase end end + def test_purpose_metadata_for_encrypted_cookies + get :encrypted_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_equal 50, cookies.encrypted[:discount_percentage] + + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :encrypted_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_nil cookies.encrypted[:discount_percentage] + end + + def test_purpose_metadata_for_signed_cookies + get :signed_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_equal 50, cookies.signed[:discount_percentage] + + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :signed_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_nil cookies.signed[:discount_percentage] + end + + def test_switch_off_metadata_for_encrypted_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :encrypted_discount_and_user_id_cookie + + travel 2.hours + assert_equal 50, cookies.encrypted[:user_id] + + cookies[:discount_percentage] = cookies[:user_id] + assert_not_equal 10, cookies.encrypted[:discount_percentage] + assert_equal 50, cookies.encrypted[:discount_percentage] + end + + def test_switch_off_metadata_for_signed_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :signed_discount_and_user_id_cookie + + travel 2.hours + assert_equal 50, cookies.signed[:user_id] + + cookies[:discount_percentage] = cookies[:user_id] + assert_not_equal 10, cookies.signed[:discount_percentage] + assert_equal 50, cookies.signed[:discount_percentage] + end + + def test_read_rails_5_2_stable_encrypted_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + + travel 1001.years do + assert_nil cookies.encrypted[:favorite] + end + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + end + + def test_read_rails_5_2_stable_signed_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + + travel 1001.years do + assert_nil cookies.signed[:favorite] + end + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + end + + def test_read_rails_5_2_stable_encrypted_cookies_if_use_metadata_config_is_true + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + + travel 1001.years do + assert_nil cookies.encrypted[:favorite] + end + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + end + + def test_read_rails_5_2_stable_signed_cookies_if_use_metadata_config_is_true + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + + travel 1001.years do + assert_nil cookies.signed[:favorite] + end + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + end + private def assert_cookie_header(expected) header = @response.headers["Set-Cookie"] diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 44b79c0e5d..37399cfd07 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -368,7 +368,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest }) assert_response 500 - assert_includes(body, CGI.escapeHTML(PP.pp(params, "".dup, 200))) + assert_includes(body, CGI.escapeHTML(PP.pp(params, +"", 200))) end test "sets the HTTP charset parameter" do diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb index 600280d6b3..668469a01d 100644 --- a/actionpack/test/dispatch/exception_wrapper_test.rb +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -20,6 +20,7 @@ module ActionDispatch setup do @cleaner = ActiveSupport::BacktraceCleaner.new + @cleaner.remove_filters! @cleaner.add_silencer { |line| line !~ /^lib/ } end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index e9f7ad41dd..5f43e5a3c5 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -42,7 +42,7 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "use should push middleware class with block arguments onto the stack" do - proc = Proc.new {} + proc = Proc.new { } assert_difference "@stack.size" do @stack.use(BlockMiddleware, &proc) end diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 85ea04356a..7a7a201b11 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -13,7 +13,7 @@ module TestGenerationPrefix end def self.model_name - klass = "Post".dup + klass = +"Post" def klass.name; self end ActiveModel::Name.new(klass) diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 84a2d1f69e..9d1246b3a4 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -24,7 +24,7 @@ class BaseRequestTest < ActiveSupport::TestCase def stub_request(env = {}) ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true @trusted_proxies ||= nil - ip_app = ActionDispatch::RemoteIp.new(Proc.new {}, ip_spoofing_check, @trusted_proxies) + ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) ActionDispatch::Http::URL.tld_length = env.delete(:tld_length) if env.key?(:tld_length) ip_app.call(env) @@ -1059,44 +1059,9 @@ class RequestParameters < BaseRequestTest end class RequestParameterFilter < BaseRequestTest - test "process parameter filter" do - test_hashes = [ - [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], - [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'], - [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'], - [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'], - [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'], - [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'], - [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], - [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]] - - test_hashes.each do |before_filter, after_filter, filter_words| - parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - assert_equal after_filter, parameter_filter.filter(before_filter) - - filter_words << "blah" - filter_words << lambda { |key, value| - value.reverse! if key =~ /bargain/ - } - - parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo" } } } - after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]" } } } - - assert_equal after_filter, parameter_filter.filter(before_filter) - end - end - - test "parameter filter should maintain hash with indifferent access" do - test_hashes = [ - [{ "foo" => "bar" }.with_indifferent_access, ["blah"]], - [{ "foo" => "bar" }.with_indifferent_access, []] - ] - - test_hashes.each do |before_filter, filter_words| - parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, - parameter_filter.filter(before_filter) + test "parameter filter is deprecated" do + assert_deprecated do + ActionDispatch::Http::ParameterFilter.new(["blah"]) end end diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index f1f6547889..fe1f1995d8 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -373,7 +373,7 @@ module ActionDispatch end def test_not_routes_when_expanded - output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) {} + output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) { } assert_equal [ "You don't have any routes defined!", @@ -450,7 +450,7 @@ module ActionDispatch end def test_no_routes_were_defined - output = draw(grep: "Rails::DummyController") {} + output = draw(grep: "Rails::DummyController") { } assert_equal [ "You don't have any routes defined!", diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 5efbe5b553..affc2d8497 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -115,6 +115,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 301, status end + def test_accepts_a_constraint_object_responding_to_call + constraint = Class.new do + def call(*); true; end + def matches?(*); false; end + end + + draw do + get "/", to: "home#show", constraints: constraint.new + end + + assert_nothing_raised do + get "/" + end + end + def test_namespace_with_controller_segment assert_raise(ArgumentError) do draw do @@ -1367,6 +1382,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal "projects#index", @response.body end + def test_optionally_scoped_root_unscoped_access + draw do + scope "(:locale)" do + scope "(:platform)" do + scope "(:browser)" do + root to: "projects#index" + end + end + end + end + + assert_equal "/", root_path + get "/" + assert_equal "projects#index", @response.body + end + def test_scope_with_format_option draw do get "direct/index", as: :no_format_direct, format: false diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 6b69cd9999..d44aa00122 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -31,7 +31,7 @@ module StaticTests end def test_handles_urls_with_ascii_8bit - assert_equal "Hello, World!", get("/doorkeeper%E3E4".dup.force_encoding("ASCII-8BIT")).body + assert_equal "Hello, World!", get((+"/doorkeeper%E3E4").force_encoding("ASCII-8BIT")).body end def test_handles_urls_with_ascii_8bit_on_win_31j @@ -39,7 +39,7 @@ module StaticTests Encoding.default_internal = "Windows-31J" Encoding.default_external = "Windows-31J" end - assert_equal "Hello, World!", get("/doorkeeper%E3E4".dup.force_encoding("ASCII-8BIT")).body + assert_equal "Hello, World!", get((+"/doorkeeper%E3E4").force_encoding("ASCII-8BIT")).body end def test_handles_urls_with_null_byte diff --git a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb index 3aadb6145e..c1a995af5f 100644 --- a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb +++ b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FooHelper - redefine_method(:baz) {} + redefine_method(:baz) { } end diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index 2d09098f11..472f1bf35e 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -23,7 +23,7 @@ module ActionDispatch end def test_uri_unescape_with_utf8_string - assert_equal "Šašinková", Utils.unescape_uri("%C5%A0a%C5%A1inkov%C3%A1".dup.force_encoding(Encoding::US_ASCII)) + assert_equal "Šašinková", Utils.unescape_uri((+"%C5%A0a%C5%A1inkov%C3%A1").force_encoding(Encoding::US_ASCII)) end def test_normalize_path_not_greedy diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 6d45cc1d8a..1f537e725a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,31 @@ +* Add allocations to template rendering instrumentation. + + Adds the allocations for template and partial rendering to the server output on render. + + ``` + Rendered posts/_form.html.erb (Duration: 7.1ms | Allocations: 6004) + Rendered posts/new.html.erb within layouts/application (Duration: 8.3ms | Allocations: 6654) + Completed 200 OK in 858ms (Views: 848.4ms | ActiveRecord: 0.4ms | Allocations: 1539564) + ``` + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Respect the `only_path` option passed to `url_for` when the options are passed in as an array + + Fixes #33237. + + *Joel Ambass* + +* Deprecate calling private model methods from view helpers. + + For example, in methods like `options_from_collection_for_select` + and `collection_select` it is possible to call private methods from + the objects used. + + Fixes #33546. + + *Ana María Martínez Gómez* + * Fix issue with `button_to`'s `to_form_params` `button_to` was throwing exception when invoked with `params` hash that @@ -66,7 +94,9 @@ *Simon Coffey* * Extract the `confirm` call in its own, overridable method in `rails_ujs`. - Example : + + Example: + Rails.confirm = function(message, element) { return (my_bootstrap_modal_confirm(message)); } @@ -74,7 +104,9 @@ *Mathieu Mahé* * Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required` - field. Example: + field. + + Example: select :post, :category, @@ -82,7 +114,9 @@ { selected: "", disabled: "", prompt: "Choose one" }, { required: true } - Placeholder option would be selected and disabled. The HTML produced: + Placeholder option would be selected and disabled. + + The HTML produced: <select required="required" name="post[category]" id="post_category"> <option disabled="disabled" selected="selected" value="">Choose one</option> diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 49ee1a292b..5f1e746421 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -26,6 +26,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionview/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "builder", "~> 3.1" diff --git a/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee index 90aa3bdf0e..a8c692ee62 100644 --- a/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee @@ -8,7 +8,12 @@ Rails.handleDisabledElement = (e) -> # Unified function to enable an element (link, button and form) Rails.enableElement = (e) -> - element = if e instanceof Event then e.target else e + if e instanceof Event + return if isXhrRedirect(e) + element = e.target + else + element = e + if matches(element, Rails.linkDisableSelector) enableLinkElement(element) else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formEnableSelector) @@ -80,3 +85,7 @@ enableFormElement = (element) -> setData(element, 'ujs:enable-with', null) # clean up cache element.disabled = false setData(element, 'ujs:disabled', null) + +isXhrRedirect = (event) -> + xhr = event.detail?[0] + xhr?.getResponseHeader("X-Xhr-Redirect")? diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb index 2a378fdc3c..18eaee5d79 100644 --- a/actionview/lib/action_view/buffers.rb +++ b/actionview/lib/action_view/buffers.rb @@ -3,6 +3,21 @@ require "active_support/core_ext/string/output_safety" module ActionView + # Used as a buffer for views + # + # The main difference between this and ActiveSupport::SafeBuffer + # is for the methods `<<` and `safe_expr_append=` the inputs are + # checked for nil before they are assigned and `to_s` is called on + # the input. For example: + # + # obuf = ActionView::OutputBuffer.new "hello" + # obuf << 5 + # puts obuf # => "hello5" + # + # sbuf = ActiveSupport::SafeBuffer.new "hello" + # sbuf << 5 + # puts sbuf # => "hello\u0005" + # class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: def initialize(*) super diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 39cdecb9e4..6d2e471a44 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -18,9 +18,12 @@ module ActionView # * <tt>name</tt> - Template name # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views - def digest(name:, finder:, dependencies: []) - dependencies ||= [] - cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") + def digest(name:, finder:, dependencies: nil) + if dependencies.nil? || dependencies.empty? + cache_key = "#{name}.#{finder.rendered_format}" + else + cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") + end # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) @@ -30,7 +33,7 @@ module ActionView root = tree(name, finder, partial) dependencies.each do |injected_dep| root.children << Injected.new(injected_dep, nil, nil) - end + end if dependencies finder.digest_cache[cache_key] = root.digest(finder) end end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 14bd8ffa84..3d7c8dae75 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -55,7 +55,7 @@ module ActionView # that path. # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline # when it is set to true. - # * <tt>:nonce<tt> - When set to true, adds an automatic nonce value if + # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if # you have Content Security Policy enabled. # # ==== Examples @@ -98,7 +98,7 @@ module ActionView if tag_options["nonce"] == true tag_options["nonce"] = content_security_policy_nonce end - content_tag("script".freeze, "", tag_options) + content_tag("script", "", tag_options) }.join("\n").html_safe request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request @@ -375,7 +375,7 @@ module ActionView def image_alt(src) ActiveSupport::Deprecation.warn("image_alt is deprecated and will be removed from Rails 6.0. You must explicitly set alt text on images.") - File.basename(src, ".*".freeze).sub(/-[[:xdigit:]]{32,64}\z/, "".freeze).tr("-_".freeze, " ".freeze).capitalize + File.basename(src, ".*").sub(/-[[:xdigit:]]{32,64}\z/, "").tr("-_", " ").capitalize end # Returns an HTML video tag for the +sources+. If +sources+ is a string, diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 1808765666..cc62783d60 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -188,7 +188,7 @@ module ActionView return "" if source.blank? return source if URI_REGEXP.match?(source) - tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "".freeze) + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "") if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 15d187a9ec..b1a14250c3 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -208,27 +208,35 @@ module ActionView # # The digest will be generated using +virtual_path:+ if it is provided. # - def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil) + def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil, digest_path: nil) if skip_digest name else - fragment_name_with_digest(name, virtual_path) + fragment_name_with_digest(name, virtual_path, digest_path) + end + end + + def digest_path_from_virtual(virtual_path) # :nodoc: + digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies) + + if digest.present? + "#{virtual_path}:#{digest}" + else + virtual_path end end private - def fragment_name_with_digest(name, virtual_path) + def fragment_name_with_digest(name, virtual_path, digest_path) virtual_path ||= @virtual_path - if virtual_path + if virtual_path || digest_path name = controller.url_for(name).split("://").last if name.is_a?(Hash) - if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence - [ "#{virtual_path}:#{digest}", name ] - else - [ virtual_path, name ] - end + digest_path ||= digest_path_from_virtual(virtual_path) + + [ digest_path, name ] else name end diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index 92f7ddb70d..c87c212cc7 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -36,6 +36,10 @@ module ActionView # </body> # </html> # + # The return of capture is the string generated by the block. For Example: + # + # @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500" + # def capture(*args) value = nil buffer = with_output_buffer { value = yield(*args) } diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 4de4fafde0..9d5e5eaba3 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -684,7 +684,7 @@ module ActionView format = options.delete(:format) || :long content = args.first || I18n.l(date_or_time, format: format) - content_tag("time".freeze, content, options.reverse_merge(datetime: date_or_time.iso8601), &block) + content_tag("time", content, options.reverse_merge(datetime: date_or_time.iso8601), &block) end private @@ -703,7 +703,7 @@ module ActionView class DateTimeSelector #:nodoc: include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = "date".freeze + DEFAULT_PREFIX = "date" POSITION = { year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6 }.freeze @@ -824,7 +824,7 @@ module ActionView 1.upto(12) do |month_number| options = { value: month_number } options[:selected] = "selected" if month == month_number - month_options << content_tag("option".freeze, month_name(month_number), options) + "\n" + month_options << content_tag("option", month_name(month_number), options) + "\n" end build_select(:month, month_options.join) end @@ -1006,7 +1006,7 @@ module ActionView tag_options[:selected] = "selected" if selected == i text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value text = options[:ampm] ? AMPM_TRANSLATION[i] : text - select_options << content_tag("option".freeze, text, tag_options) + select_options << content_tag("option", text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -1034,7 +1034,7 @@ module ActionView tag_options = { value: value } tag_options[:selected] = "selected" if selected == value text = year_name(value) - select_options << content_tag("option".freeze, text, tag_options) + select_options << content_tag("option", text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -1053,12 +1053,12 @@ module ActionView select_options[:disabled] = "disabled" if @options[:disabled] select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes] - select_html = "\n".dup - select_html << content_tag("option".freeze, "", value: "") + "\n" if @options[:include_blank] + select_html = +"\n" + select_html << content_tag("option", "", value: "") + "\n" if @options[:include_blank] select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html - (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe + (content_tag("select", select_html.html_safe, select_options) + "\n").html_safe end # Builds the css class value for the select element @@ -1091,7 +1091,7 @@ module ActionView I18n.translate(:"datetime.prompts.#{type}", locale: @options[:locale]) end - prompt ? content_tag("option".freeze, prompt, value: "") : "" + prompt ? content_tag("option", prompt, value: "") : "" end # Builds hidden input tag for date part and value. @@ -1135,7 +1135,7 @@ module ActionView # Given an ordering of datetime components, create the selection HTML # and join them with their appropriate separators. def build_selects_from_types(order) - select = "".dup + select = +"" first_visible = order.find { |type| !@options[:"discard_#{type}"] } order.reverse_each do |type| separator = separator(type) unless type == first_visible # don't add before first visible field diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 07f3d98322..c2caa77afb 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -590,6 +590,9 @@ module ActionView # Skipped if a <tt>:url</tt> is passed. # * <tt>:scope</tt> - The scope to prefix input field names with and # thereby how the submitted parameters are grouped in controllers. + # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of + # id attributes on form elements. The namespace attribute will be prefixed + # with underscore on the generated HTML id. # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and # <tt>:scope</tt> by, plus fill out input field values. # So if a +title+ attribute is set to "Ahoy!" then a +title+ input @@ -1127,6 +1130,9 @@ module ActionView # text_field(:post, :title, class: "create_input") # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> # + # text_field(:post, :title, maxlength: 30, class: "title_input") + # # => <input type="text" id="post_title" name="post[title]" maxlength="30" size="30" value="#{@post.title}" class="title_input" /> + # # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }") # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/> # diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 7884a8d997..ebdd96f570 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -463,7 +463,7 @@ module ActionView option_tags = options_from_collection_for_select( value_for_collection(group, group_method), option_key_method, option_value_method, selected_key) - content_tag("optgroup".freeze, option_tags, label: value_for_collection(group, group_label_method)) + content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method)) end.join.html_safe end @@ -535,7 +535,7 @@ module ActionView body = "".html_safe if prompt - body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "") + body.safe_concat content_tag("option", prompt_text(prompt), value: "") end grouped_options.each do |container| @@ -548,7 +548,7 @@ module ActionView end html_attributes = { label: label }.merge!(html_attributes) - body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes) + body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes) end body @@ -584,7 +584,7 @@ module ActionView end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) - zone_options.safe_concat content_tag("option".freeze, "-------------", value: "", disabled: true) + zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true) zone_options.safe_concat "\n" zones = zones - priority_zones @@ -794,7 +794,7 @@ module ActionView def extract_values_from_collection(collection, value_method, selected) if selected.is_a?(Proc) collection.map do |element| - element.send(value_method) if selected.call(element) + public_or_deprecated_send(element, value_method) if selected.call(element) end.compact else selected @@ -802,7 +802,15 @@ module ActionView end def value_for_collection(item, value) - value.respond_to?(:call) ? value.call(item) : item.send(value) + value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value) + end + + def public_or_deprecated_send(item, value) + item.public_send(value) + rescue NoMethodError + raise unless item.respond_to?(value, true) && !item.respond_to?(value) + ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})" + item.send(value) end def prompt_text(prompt) diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index ba09738beb..c0996049f0 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -146,15 +146,15 @@ module ActionView end if include_blank - option_tags = content_tag("option".freeze, include_blank, options_for_blank_options_tag).safe_concat(option_tags) + option_tags = content_tag("option", include_blank, options_for_blank_options_tag).safe_concat(option_tags) end end if prompt = options.delete(:prompt) - option_tags = content_tag("option".freeze, prompt, value: "").safe_concat(option_tags) + option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags) end - content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) + content_tag "select", option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) end # Creates a standard text field; use these text fields to input smaller chunks of text like a username @@ -577,7 +577,7 @@ module ActionView # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset> def field_set_tag(legend = nil, options = nil, &block) output = tag(:fieldset, options, true) - output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank? + output.safe_concat(content_tag("legend", legend)) unless legend.blank? output.concat(capture(&block)) if block_given? output.safe_concat("</fieldset>") end diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index 830088bea3..b680cb1bd3 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -15,8 +15,8 @@ module ActionView "'" => "\\'" } - JS_ESCAPE_MAP["\342\200\250".dup.force_encoding(Encoding::UTF_8).encode!] = "
" - JS_ESCAPE_MAP["\342\200\251".dup.force_encoding(Encoding::UTF_8).encode!] = "
" + JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "
" + JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "
" # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -25,12 +25,13 @@ module ActionView # # $('some_element').replaceWith('<%= j render 'some/element_template' %>'); def escape_javascript(javascript) - if javascript - result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) { |match| JS_ESCAPE_MAP[match] } - javascript.html_safe? ? result.html_safe : result + javascript = javascript.to_s + if javascript.empty? + result = "" else - "" + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) { |match| JS_ESCAPE_MAP[match] } end + javascript.html_safe? ? result.html_safe : result end alias_method :j, :escape_javascript @@ -83,7 +84,7 @@ module ActionView html_options[:nonce] = content_security_policy_nonce end - content_tag("script".freeze, javascript_cdata_section(content), html_options) + content_tag("script", javascript_cdata_section(content), html_options) end def javascript_cdata_section(content) #:nodoc: diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 4b53b8fe6e..35206b7e48 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -100,6 +100,9 @@ module ActionView # absolute value of the number. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). # # ==== Examples # @@ -117,6 +120,8 @@ module ActionView # # => R$1234567890,50 # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u") # # => 1234567890,50 R$ + # number_to_currency(1234567890.50, strip_insignificant_zeros: true) + # # => "$1,234,567,890.5" def number_to_currency(number, options = {}) delegate_number_helper_method(:number_to_currency, number, options) end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index cb0c99c4cf..f4fa133f55 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -10,7 +10,7 @@ module ActionView # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted. + # Sanitizes HTML input, stripping all but known-safe tags and attributes. # # It also strips href/src attributes with unsafe protocols like # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, @@ -40,7 +40,7 @@ module ActionView # # <%= sanitize @comment.body %> # - # Providing custom whitelisted tags and attributes: + # Providing custom lists of permitted tags and attributes: # # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %> # diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index d12989ea64..3979721d34 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -58,7 +58,7 @@ module ActionView def tag_options(options, escape = true) return if options.blank? - output = "".dup + output = +"" sep = " " options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) @@ -86,11 +86,11 @@ module ActionView def tag_option(key, value, escape) if value.is_a?(Array) - value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) + value = escape ? safe_join(value, " ") : value.join(" ") else value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s.dup end - value.gsub!('"'.freeze, """.freeze) + value.gsub!('"', """) %(#{key}="#{value}") end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 77a1c1fed9..a338d076e4 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -228,7 +228,7 @@ module ActionView # pluralize(2, 'Person', locale: :de) # # => 2 Personen def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale) - word = if (count == 1 || count =~ /^1(\.0+)?$/) + word = if count == 1 || count =~ /^1(\.0+)?$/ singular else plural || singular.pluralize(locale) diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index ba82dcab3e..ae1c93e12f 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -98,7 +98,7 @@ module ActionView raise e if raise_error keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - title = "translation missing: #{keys.join('.')}".dup + title = +"translation missing: #{keys.join('.')}" interpolations = options.except(:default, :scope) if interpolations.any? diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 52bffaab84..948dd1551f 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -200,9 +200,9 @@ module ActionView html_options = convert_options_to_data_attributes(options, html_options) url = url_for(options) - html_options["href".freeze] ||= url + html_options["href"] ||= url - content_tag("a".freeze, name || url, html_options, &block) + content_tag("a", name || url, html_options, &block) end # Generates a form containing a single button that submits to the URL created @@ -308,7 +308,7 @@ module ActionView params = html_options.delete("params") method = html_options.delete("method").to_s - method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".freeze.html_safe + method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe form_method = method == "get" ? "get" : "post" form_options = html_options.delete("form") || {} @@ -321,7 +321,7 @@ module ActionView request_method = method.empty? ? "post" : method token_tag(nil, form_options: { action: url, method: request_method }) else - "".freeze + "" end html_options = convert_options_to_data_attributes(options, html_options) @@ -487,12 +487,12 @@ module ActionView option = html_options.delete(item).presence || next "#{item.dasherize}=#{ERB::Util.url_encode(option)}" }.compact - extras = extras.empty? ? "".freeze : "?" + extras.join("&") + extras = extras.empty? ? "" : "?" + extras.join("&") encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") html_options["href"] = "mailto:#{encoded_email_address}#{extras}" - content_tag("a".freeze, name || email_address, html_options, &block) + content_tag("a", name || email_address, html_options, &block) end # True if the current request URI was generated by the given +options+. @@ -575,21 +575,21 @@ module ActionView def convert_options_to_data_attributes(options, html_options) if html_options html_options = html_options.stringify_keys - html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) + html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete("method".freeze) + method = html_options.delete("method") add_method_to_attributes!(html_options, method) if method html_options else - link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {} + link_to_remote_options?(options) ? { "data-remote" => "true" } : {} end end def link_to_remote_options?(options) if options.is_a?(Hash) - options.delete("remote".freeze) || options.delete(:remote) + options.delete("remote") || options.delete(:remote) end end @@ -622,7 +622,7 @@ module ActionView token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) else - "".freeze + "" end end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index d4ac77e10f..227f025385 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -16,17 +16,17 @@ module ActionView def render_template(event) info do - message = " Rendered #{from_rails_root(event.payload[:identifier])}".dup + message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end def render_partial(event) info do - message = " Rendered #{from_rails_root(event.payload[:identifier])}".dup + message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil? message end @@ -37,7 +37,7 @@ module ActionView info do " Rendered collection of #{from_rails_root(identifier)}" \ - " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end @@ -85,7 +85,7 @@ module ActionView def log_rendering_start(payload) info do - message = " Rendering #{from_rails_root(payload[:identifier])}".dup + message = +" Rendering #{from_rails_root(payload[:identifier])}" message << " within #{from_rails_root(payload[:layout])}" if payload[:layout] message end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 0e56eca35c..af67ffa12d 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -202,13 +202,13 @@ module ActionView # name instead of the prefix. def normalize_name(name, prefixes) prefixes = prefixes.presence - parts = name.to_s.split("/".freeze) + parts = name.to_s.split("/") parts.shift if parts.first.empty? name = parts.pop return name, prefixes || [""] if parts.empty? - parts = parts.join("/".freeze) + parts = parts.join("/") prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -245,7 +245,7 @@ module ActionView # add :html as fallback to :js. def formats=(values) if values - values.concat(default_formats) if values.delete "*/*".freeze + values.concat(default_formats) if values.delete "*/*" if values == [:js] values << :html @html_fallback_for_js = true diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 1310a1ce0a..ee39b6050d 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -59,8 +59,8 @@ module ActionView include ModelNaming - JOIN = "_".freeze - NEW = "new".freeze + JOIN = "_" + NEW = "new" # The DOM class convention is to use the singular form of an object or class. # diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index d7f97c3b50..cb850d75ee 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -523,7 +523,7 @@ module ActionView def retrieve_variable(path, as) variable = as || begin - base = path[-1] == "/".freeze ? "".freeze : File.basename(path) + base = path[-1] == "/" ? "" : File.basename(path) raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/ $1.to_sym end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index db52919e91..5aa6f77902 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -14,15 +14,35 @@ module ActionView def cache_collection_render(instrumentation_payload) return yield unless @options[:cached] + # Result is a hash with the key represents the + # key used for cache lookup and the value is the item + # on which the partial is being rendered keyed_collection = collection_by_cache_keys + + # Pull all partials from cache + # Result is a hash, key matches the entry in + # `keyed_collection` where the cache was retrieved and the + # value is the value that was present in the cache cached_partials = collection_cache.read_multi(*keyed_collection.keys) instrumentation_payload[:cache_hits] = cached_partials.size + # Extract the items for the keys that are not found + # Set the uncached values to instance variable @collection + # which is used by the caller @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + + # If all elements are already in cache then + # rendered partials will be an empty array + # + # If the cache is missing elements then + # the block will be called against the remaining items + # in the @collection. rendered_partials = @collection.empty? ? [] : yield index = 0 fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + # This block is called once + # for every cache miss while preserving order. rendered_partials[index].tap { index += 1 } end end @@ -40,10 +60,29 @@ module ActionView end def expanded_cache_key(key) - key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) + key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end + def digest_path + @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path) + end + + # `order_by` is an enumerable object containing keys of the cache, + # all keys are passed in whether found already or not. + # + # `cached_partials` is a hash. If the value exists + # it represents the rendered partial from the cache + # otherwise `Hash#fetch` will take the value of its block. + # + # This method expects a block that will return the rendered + # partial. An example is to render all results + # for each element that was not found in the cache and store it as an array. + # Order it so that the first empty cache element in `cached_partials` + # corresponds to the first element in `rendered_partials`. + # + # If the partial is not already cached it will also be + # written back to the underlying cache store. def fetch_or_cache_partial(cached_partials, order_by:) order_by.map do |cache_key| cached_partials.fetch(cache_key) do diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 276a28ce07..bb9db21e32 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -33,7 +33,7 @@ module ActionView logger = ActionView::Base.logger return unless logger - message = "\n#{exception.class} (#{exception.message}):\n".dup + message = +"\n#{exception.class} (#{exception.message}):\n" message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) message << " " << exception.backtrace.join("\n ") logger.fatal("#{message}\n\n") diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index fd563f34a9..f8ea3aa770 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -84,25 +84,24 @@ module ActionView super(only_path: _generate_paths_by_default) when Hash options = options.symbolize_keys - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when ActionController::Parameters - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when :back _back_url when Array components = options.dup - if _generate_paths_by_default - polymorphic_path(components, components.extract_options!) + options = components.extract_options! + ensure_only_path_option(options) + + if options[:only_path] + polymorphic_path(components, options) else - polymorphic_url(components, components.extract_options!) + polymorphic_url(components, options) end else method = _generate_paths_by_default ? :path : :url @@ -138,8 +137,10 @@ module ActionView true end - def only_path?(host) - _generate_paths_by_default unless host + def ensure_only_path_option(options) + unless options.key?(:only_path) + options[:only_path] = _generate_paths_by_default unless options[:host] + end end end end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index ee1cd61f12..070d82cf17 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -188,7 +188,7 @@ module ActionView end def inspect - @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "".freeze) : identifier + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier end # This method is responsible for properly setting the encoding of the @@ -235,6 +235,19 @@ module ActionView end end + + # Exceptions are marshalled when using the parallel test runner with DRb, so we need + # to ensure that references to the template object can be marshalled as well. This means forgoing + # the marshalling of the compiler mutex and instantiating that again on unmarshalling. + def marshal_dump # :nodoc: + [ @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ] + end + + def marshal_load(array) # :nodoc: + @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array + @compile_mutex = Mutex.new + end + private # Compile a template. This method ensures a template is compiled @@ -286,7 +299,7 @@ module ActionView # Make sure that the resulting String to be eval'd is in the # encoding of the code - source = <<-end_src.dup + source = +<<-end_src def #{method_name}(local_assigns, output_buffer) _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} ensure @@ -335,19 +348,19 @@ module ActionView locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/) # Assign for the same variable is to suppress unused variable warning - locals.each_with_object("".dup) { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" } + locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" } end def method_name @method_name ||= begin - m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".dup - m.tr!("-".freeze, "_".freeze) + m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" + m.tr!("-", "_") m end end def identifier_method_name - inspect.tr("^a-z_".freeze, "_".freeze) + inspect.tr("^a-z_", "_") end def instrument(action, &block) # :doc: @@ -355,7 +368,7 @@ module ActionView end def instrument_render_template(&block) - ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, instrument_payload, &block) + ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block) end def instrument_payload diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index b7b749f9da..270be0a380 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -14,7 +14,17 @@ module ActionView class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist, default: ["text/plain"] + class_attribute :escape_ignore_list, default: ["text/plain"] + + [self, singleton_class].each do |base| + base.send(:alias_method, :escape_whitelist, :escape_ignore_list) + base.send(:alias_method, :escape_whitelist=, :escape_ignore_list=) + + base.deprecate( + escape_whitelist: "use #escape_ignore_list instead", + :escape_whitelist= => "use #escape_ignore_list= instead" + ) + end ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") @@ -47,7 +57,7 @@ module ActionView self.class.erb_implementation.new( erb, - escape: (self.class.escape_whitelist.include? template.type), + escape: (self.class.escape_ignore_list.include? template.type), trim: (self.class.erb_trim_mode == "-") ).src end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 5a86f10973..08dd6fb510 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -16,7 +16,7 @@ module ActionView alias_method :partial?, :partial def self.build(name, prefix, partial) - virtual = "".dup + virtual = +"" virtual << "#{prefix}/" unless prefix.empty? virtual << (partial ? "_#{name}" : name) new name, prefix, partial, virtual @@ -221,9 +221,7 @@ module ActionView end def query(path, details, formats, outside_app_allowed) - query = build_query(path, details) - - template_paths = find_template_paths(query) + template_paths = find_template_paths_from_details(path, details) template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed template_paths.map do |template| @@ -243,6 +241,11 @@ module ActionView files.reject { |filename| !inside_path?(@path, filename) } end + def find_template_paths_from_details(path, details) + query = build_query(path, details) + find_template_paths(query) + end + def find_template_paths(query) Dir[query].uniq.reject do |filename| File.directory?(filename) || @@ -279,7 +282,7 @@ module ActionView end def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') end # Returns the file mtime from the filesystem. @@ -291,7 +294,7 @@ module ActionView # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format_and_variant(path) - pieces = File.basename(path).split(".".freeze) + pieces = File.basename(path).split(".") pieces.shift extension = pieces.pop @@ -362,19 +365,56 @@ module ActionView # An Optimized resolver for Rails' most common case. class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: - def build_query(path, details) - query = escape_entry(File.join(@path, path)) + private - exts = EXTENSIONS.map do |ext, prefix| - if ext == :variants && details[ext] == :any - "{#{prefix}*,}" - else - "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + def find_template_paths_from_details(path, details) + # Instead of checking for every possible path, as our other globs would + # do, scan the directory for files with the right prefix. + query = "#{escape_entry(File.join(@path, path))}*" + + regex = build_regex(path, details) + + Dir[query].uniq.reject do |filename| + # This regex match does double duty of finding only files which match + # details (instead of just matching the prefix) and also filtering for + # case-insensitive file systems. + !filename.match(regex) || + File.directory?(filename) + end.sort_by do |filename| + # Because we scanned the directory, instead of checking for files + # one-by-one, they will be returned in an arbitrary order. + # We can use the matches found by the regex and sort by their index in + # details. + match = filename.match(regex) + EXTENSIONS.keys.reverse.map do |ext| + if ext == :variants && details[ext] == :any + match[ext].nil? ? 0 : 1 + elsif match[ext].nil? + # No match should be last + details[ext].length + else + found = match[ext].to_sym + details[ext].index(found) + end + end end - end.join + end - query + exts - end + def build_regex(path, details) + query = escape_entry(File.join(@path, path)) + exts = EXTENSIONS.map do |ext, prefix| + match = + if ext == :variants && details[ext] == :any + ".*?" + else + details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|") + end + prefix = Regexp.escape(prefix) + "(#{prefix}(?<#{ext}>#{match}))?" + end.join + + %r{\A#{query}#{exts}\z} + end end # The same as FileSystemResolver but does not allow templates to store diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index e1cbae5845..e14f7aaec7 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -107,7 +107,7 @@ module ActionView # empty string ensures buffer has UTF-8 encoding as # new without arguments returns ASCII-8BIT encoded buffer like String#new @output_buffer = ActiveSupport::SafeBuffer.new "" - @rendered = "".dup + @rendered = +"" make_test_case_available_to_view! say_no_to_protect_against_forgery! diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 68186c3bf8..1fad08a689 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -22,7 +22,7 @@ module ActionView #:nodoc: private def query(path, exts, _, _) - query = "".dup + query = +"" EXTENSIONS.each_key do |ext| query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" end diff --git a/actionview/package.json b/actionview/package.json index 624eb5de93..1f74df79d3 100644 --- a/actionview/package.json +++ b/actionview/package.json @@ -30,7 +30,7 @@ }, "homepage": "http://rubyonrails.org/", "devDependencies": { - "coffeelint": "^1.15.7", + "coffeelint": "^2.1.0", "eslint": "^2.13.1" } } diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index f20a66c2d2..f90626ad9e 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -65,43 +65,6 @@ module RenderERBUtils end end -SharedTestRoutes = ActionDispatch::Routing::RouteSet.new - -module ActionDispatch - module SharedRoutes - def before_setup - @routes = SharedTestRoutes - super - end - end - - # Hold off drawing routes until all the possible controller classes - # have been loaded. - module DrawOnce - class << self - attr_accessor :drew - end - self.drew = false - - def before_setup - super - return if DrawOnce.drew - - ActiveSupport::Deprecation.silence do - SharedTestRoutes.draw do - get ":controller(/:action)" - end - - ActionDispatch::IntegrationTest.app.routes.draw do - get ":controller(/:action)" - end - end - - DrawOnce.drew = true - end - end -end - class RoutedRackApp attr_reader :routes @@ -132,10 +95,11 @@ class BasicController end class ActionDispatch::IntegrationTest < ActiveSupport::TestCase - include ActionDispatch::SharedRoutes - def self.build_app(routes = nil) - RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| + routes ||= ActionDispatch::Routing::RouteSet.new.tap { |rs| + rs.draw { } + } + RoutedRackApp.new(routes) do |middleware| middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") middleware.use ActionDispatch::DebugExceptions middleware.use ActionDispatch::Callbacks @@ -151,13 +115,10 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase def with_routing(&block) temporary_routes = ActionDispatch::Routing::RouteSet.new old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes) - old_routes = SharedTestRoutes - silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) } yield temporary_routes ensure self.class.app = old_app - silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } end end @@ -165,30 +126,37 @@ ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) module ActionController class Base - # This stub emulates the Railtie including the URL helpers from a Rails application - include SharedTestRoutes.url_helpers - include SharedTestRoutes.mounted_helpers - self.view_paths = FIXTURE_LOAD_PATH def self.test_routes(&block) routes = ActionDispatch::Routing::RouteSet.new routes.draw(&block) include routes.url_helpers + routes end end class TestCase include ActionDispatch::TestProcess - include ActionDispatch::SharedRoutes - end -end -module ActionView - class TestCase - # Must repeat the setup because AV::TestCase is a duplication - # of AC::TestCase - include ActionDispatch::SharedRoutes + def self.with_routes(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + include Module.new { + define_method(:setup) do + super() + @routes = routes + @controller.singleton_class.include @routes.url_helpers + end + } + routes + end + + def with_routes(&block) + @routes = ActionDispatch::Routing::RouteSet.new + @routes.draw(&block) + @routes + end end end @@ -222,15 +190,16 @@ module ActionDispatch end class ActiveSupport::TestCase - include ActionDispatch::DrawOnce include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb index 09309e5b6d..d02125bafa 100644 --- a/actionview/test/actionpack/controller/capture_test.rb +++ b/actionview/test/actionpack/controller/capture_test.rb @@ -37,6 +37,15 @@ end class CaptureTest < ActionController::TestCase tests CaptureController + with_routes do + get :content_for, to: "test#content_for" + get :capturing, to: "test#capturing" + get :proper_block_detection, to: "test#proper_block_detection" + get :non_erb_block_content_for, to: "test#non_erb_block_content_for" + get :content_for_concatenated, to: "test#content_for_concatenated" + get :content_for_with_parameter, to: "test#content_for_with_parameter" + end + def setup super # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index ff66ff2a1a..6d5c97b7fd 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -48,6 +48,10 @@ end class LayoutAutoDiscoveryTest < ActionController::TestCase include TemplateHandlerHelper + with_routes do + get :hello, to: "views#hello" + end + def setup super @request.host = "www.nextangle.com" @@ -148,6 +152,11 @@ class LayoutSetInResponseTest < ActionController::TestCase include ActionView::Template::Handlers include TemplateHandlerHelper + with_routes do + get :hello, to: "views#hello" + get :hello, to: "views#goodbye" + end + def test_layout_set_when_using_default_layout @controller = DefaultLayoutController.new get :hello @@ -234,6 +243,10 @@ class SetsNonExistentLayoutFile < LayoutTest end class LayoutExceptionRaisedTest < ActionController::TestCase + with_routes do + get :hello, to: "views#hello" + end + def test_exception_raised_when_layout_file_not_found @controller = SetsNonExistentLayoutFile.new assert_raise(ActionView::MissingTemplate) { get :hello } @@ -247,6 +260,10 @@ class LayoutStatusIsRendered < LayoutTest end class LayoutStatusIsRenderedTest < ActionController::TestCase + with_routes do + get :hello, to: "views#hello" + end + def test_layout_status_is_rendered @controller = LayoutStatusIsRendered.new get :hello @@ -260,6 +277,10 @@ unless Gem.win_platform? end class LayoutSymlinkedIsRenderedTest < ActionController::TestCase + with_routes do + get :hello, to: "views#hello" + end + def test_symlinked_layout_is_rendered @controller = LayoutSymlinkedTest.new get :hello diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 3e6b55a87e..204903c60c 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -632,6 +632,124 @@ end class RenderTest < ActionController::TestCase tests TestController + with_routes do + get :"hyphen-ated", to: "test#hyphen-ated" + get :accessing_action_name_in_template, to: "test#accessing_action_name_in_template" + get :accessing_controller_name_in_template, to: "test#accessing_controller_name_in_template" + get :accessing_local_assigns_in_inline_template, to: "test#accessing_local_assigns_in_inline_template" + get :accessing_logger_in_template, to: "test#accessing_logger_in_template" + get :accessing_params_in_template, to: "test#accessing_params_in_template" + get :accessing_params_in_template_with_layout, to: "test#accessing_params_in_template_with_layout" + get :accessing_request_in_template, to: "test#accessing_request_in_template" + get :action_talk_to_layout, to: "test#action_talk_to_layout" + get :builder_layout_test, to: "test#builder_layout_test" + get :builder_partial_test, to: "test#builder_partial_test" + get :clone, to: "test#clone" + get :determine_layout, to: "test#determine_layout" + get :double_redirect, to: "test#double_redirect" + get :double_render, to: "test#double_render" + get :empty_partial_collection, to: "test#empty_partial_collection" + get :formatted_html_erb, to: "test#formatted_html_erb" + get :formatted_xml_erb, to: "test#formatted_xml_erb" + get :greeting, to: "test#greeting" + get :hello_in_a_string, to: "test#hello_in_a_string" + get :hello_world, to: "fun/games#hello_world" + get :hello_world, to: "test#hello_world" + get :hello_world_file, to: "test#hello_world_file" + get :hello_world_from_rxml_using_action, to: "test#hello_world_from_rxml_using_action" + get :hello_world_from_rxml_using_template, to: "test#hello_world_from_rxml_using_template" + get :hello_world_with_layout_false, to: "test#hello_world_with_layout_false" + get :layout_overriding_layout, to: "test#layout_overriding_layout" + get :layout_test, to: "test#layout_test" + get :layout_test_with_different_layout, to: "test#layout_test_with_different_layout" + get :layout_test_with_different_layout_and_string_action, to: "test#layout_test_with_different_layout_and_string_action" + get :layout_test_with_different_layout_and_symbol_action, to: "test#layout_test_with_different_layout_and_symbol_action" + get :missing_partial, to: "test#missing_partial" + get :nested_partial_with_form_builder, to: "fun/games#nested_partial_with_form_builder" + get :new, to: "quiz/questions#new" + get :partial, to: "test#partial" + get :partial_collection, to: "test#partial_collection" + get :partial_collection_shorthand_with_different_types_of_records, to: "test#partial_collection_shorthand_with_different_types_of_records" + get :partial_collection_shorthand_with_locals, to: "test#partial_collection_shorthand_with_locals" + get :partial_collection_with_as, to: "test#partial_collection_with_as" + get :partial_collection_with_as_and_counter, to: "test#partial_collection_with_as_and_counter" + get :partial_collection_with_as_and_iteration, to: "test#partial_collection_with_as_and_iteration" + get :partial_collection_with_counter, to: "test#partial_collection_with_counter" + get :partial_collection_with_iteration, to: "test#partial_collection_with_iteration" + get :partial_collection_with_locals, to: "test#partial_collection_with_locals" + get :partial_collection_with_spacer, to: "test#partial_collection_with_spacer" + get :partial_collection_with_spacer_which_uses_render, to: "test#partial_collection_with_spacer_which_uses_render" + get :partial_formats_html, to: "test#partial_formats_html" + get :partial_hash_collection, to: "test#partial_hash_collection" + get :partial_hash_collection_with_locals, to: "test#partial_hash_collection_with_locals" + get :partial_html_erb, to: "test#partial_html_erb" + get :partial_only, to: "test#partial_only" + get :partial_with_counter, to: "test#partial_with_counter" + get :partial_with_form_builder, to: "test#partial_with_form_builder" + get :partial_with_form_builder_subclass, to: "test#partial_with_form_builder_subclass" + get :partial_with_hash_object, to: "test#partial_with_hash_object" + get :partial_with_locals, to: "test#partial_with_locals" + get :partial_with_nested_object, to: "test#partial_with_nested_object" + get :partial_with_nested_object_shorthand, to: "test#partial_with_nested_object_shorthand" + get :partial_with_string_locals, to: "test#partial_with_string_locals" + get :partials_list, to: "test#partials_list" + get :render_action_hello_world, to: "test#render_action_hello_world" + get :render_action_hello_world_as_string, to: "test#render_action_hello_world_as_string" + get :render_action_hello_world_with_symbol, to: "test#render_action_hello_world_with_symbol" + get :render_action_upcased_hello_world, to: "test#render_action_upcased_hello_world" + get :render_and_redirect, to: "test#render_and_redirect" + get :render_call_to_partial_with_layout, to: "test#render_call_to_partial_with_layout" + get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout, to: "test#render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout" + get :render_custom_code, to: "test#render_custom_code" + get :render_file_as_string_with_locals, to: "test#render_file_as_string_with_locals" + get :render_file_from_template, to: "test#render_file_from_template" + get :render_file_not_using_full_path, to: "test#render_file_not_using_full_path" + get :render_file_not_using_full_path_with_dot_in_path, to: "test#render_file_not_using_full_path_with_dot_in_path" + get :render_file_using_pathname, to: "test#render_file_using_pathname" + get :render_file_with_instance_variables, to: "test#render_file_with_instance_variables" + get :render_file_with_locals, to: "test#render_file_with_locals" + get :render_hello_world, to: "test#render_hello_world" + get :render_hello_world_from_variable, to: "test#render_hello_world_from_variable" + get :render_hello_world_with_forward_slash, to: "test#render_hello_world_with_forward_slash" + get :render_implicit_html_template_from_xhr_request, to: "test#render_implicit_html_template_from_xhr_request" + get :render_implicit_js_template_without_layout, to: "test#render_implicit_js_template_without_layout" + get :render_line_offset, to: "test#render_line_offset" + get :render_nothing_with_appendix, to: "test#render_nothing_with_appendix" + get :render_template_in_top_directory, to: "test#render_template_in_top_directory" + get :render_template_in_top_directory_with_slash, to: "test#render_template_in_top_directory_with_slash" + get :render_template_within_a_template_with_other_format, to: "test#render_template_within_a_template_with_other_format" + get :render_text_hello_world, to: "test#render_text_hello_world" + get :render_text_hello_world_with_layout, to: "test#render_text_hello_world_with_layout" + get :render_text_with_assigns, to: "test#render_text_with_assigns" + get :render_text_with_false, to: "test#render_text_with_false" + get :render_text_with_nil, to: "test#render_text_with_nil" + get :render_text_with_resource, to: "test#render_text_with_resource" + get :render_to_string_and_render, to: "test#render_to_string_and_render" + get :render_to_string_and_render_with_different_formats, to: "test#render_to_string_and_render_with_different_formats" + get :render_to_string_test, to: "test#render_to_string_test" + get :render_to_string_with_assigns, to: "test#render_to_string_with_assigns" + get :render_to_string_with_caught_exception, to: "test#render_to_string_with_caught_exception" + get :render_to_string_with_exception, to: "test#render_to_string_with_exception" + get :render_to_string_with_inline_and_render, to: "test#render_to_string_with_inline_and_render" + get :render_to_string_with_partial, to: "test#render_to_string_with_partial" + get :render_to_string_with_template_and_html_partial, to: "test#render_to_string_with_template_and_html_partial" + get :render_using_layout_around_block, to: "test#render_using_layout_around_block" + get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout, to: "test#render_using_layout_around_block_in_main_layout_and_within_content_for_layout" + get :render_with_assigns_option, to: "test#render_with_assigns_option" + get :render_with_explicit_escaped_template, to: "test#render_with_explicit_escaped_template" + get :render_with_explicit_string_template, to: "test#render_with_explicit_string_template" + get :render_with_explicit_template, to: "test#render_with_explicit_template" + get :render_with_explicit_template_with_locals, to: "test#render_with_explicit_template_with_locals" + get :render_with_explicit_unescaped_template, to: "test#render_with_explicit_unescaped_template" + get :render_with_filters, to: "test#render_with_filters" + get :render_xml_hello, to: "test#render_xml_hello" + get :render_xml_hello_as_string_template, to: "test#render_xml_hello_as_string_template" + get :rendering_nothing_on_layout, to: "test#rendering_nothing_on_layout" + get :rendering_with_conflicting_local_vars, to: "test#rendering_with_conflicting_local_vars" + get :rendering_without_layout, to: "test#rendering_without_layout" + get :yield_content_for, to: "test#yield_content_for" + end + def setup # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get # a more accurate simulation of what happens in "real life". diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index 45c662f0ce..7f3fe0fa08 100644 --- a/actionview/test/actionpack/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb @@ -24,11 +24,17 @@ class ViewLoadPathsTest < ActionController::TestCase end end + with_routes do + get :hello_world, to: "test#hello_world" + get :hello_world_at_request_time, to: "test#hello_world_at_request_time" + end + def setup @controller = TestController.new @request = ActionController::TestRequest.create(@controller.class) @response = ActionDispatch::TestResponse.new @paths = TestController.view_paths + super end def teardown @@ -109,6 +115,10 @@ class ViewLoadPathsTest < ActionController::TestCase def test_view_paths_override_for_layouts_in_controllers_with_a_module @controller = Test::SubController.new + with_routes do + get :hello_world, to: "view_load_paths_test/test/sub#hello_world" + end + Test::SubController.view_paths = [ "#{FIXTURE_LOAD_PATH}/override", FIXTURE_LOAD_PATH, "#{FIXTURE_LOAD_PATH}/override2" ] get :hello_world assert_response :success diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index 7f48b515a0..e4ea6a426d 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -74,6 +74,18 @@ end class ActiveRecordTestCase < ActionController::TestCase include ActiveRecord::TestFixtures + def self.tests(controller) + super + if defined? controller::ROUTES + include Module.new { + define_method(:setup) do + super() + @routes = controller::ROUTES + end + } + end + end + # Set our fixture path if ActiveRecordTestConnector.able_to_connect self.fixture_path = [FIXTURE_LOAD_PATH] diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 42b171ea07..563044f11e 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -39,6 +39,14 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase include ActiveSupport::LogSubscriber::TestHelper tests LogSubscriberController + with_routes do + get :show, to: "#{LogSubscriberController.controller_path}#show" + get :zero, to: "#{LogSubscriberController.controller_path}#zero" + get :db_after_render, to: "#{LogSubscriberController.controller_path}#db_after_render" + get :redirect, to: "#{LogSubscriberController.controller_path}#redirect" + post :create, to: "#{LogSubscriberController.controller_path}#create" + end + def setup @old_logger = ActionController::Base.logger super @@ -60,7 +68,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1]) end def test_runtime_reset_before_requests @@ -69,20 +77,20 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0\.0ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1]) end def test_log_with_active_record_when_post post :create wait - assert_match(/ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[2]) + assert_match(/ActiveRecord: ([1-9][\d.]+)ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[2]) end def test_log_with_active_record_when_redirecting get :redirect wait assert_equal 3, @logger.logged(:info).size - assert_match(/\(ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[2]) + assert_match(/\(ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[2]) end def test_include_time_query_time_after_rendering @@ -90,6 +98,6 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1]) end end diff --git a/actionview/test/activerecord/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb index 4be1023733..87a1791573 100644 --- a/actionview/test/activerecord/debug_helper_test.rb +++ b/actionview/test/activerecord/debug_helper_test.rb @@ -13,7 +13,7 @@ class DebugHelperTest < ActionView::TestCase end def test_debug_with_marshal_error - obj = -> {} + obj = -> { } assert_match obj.inspect, Nokogiri.XML(debug(obj)).content end end diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 1472ee8def..34655bfe23 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -23,9 +23,12 @@ class FormHelperActiveRecordTest < ActionView::TestCase @developer.projects << @project @developer.save + super + @controller.singleton_class.include Routes.url_helpers end def teardown + super Project.delete(321) Developer.delete(123) end @@ -57,7 +60,7 @@ class FormHelperActiveRecordTest < ActionView::TestCase private def hidden_fields(method = nil) - txt = %{<input name="utf8" type="hidden" value="✓" />}.dup + txt = +%{<input name="utf8" type="hidden" value="✓" />} if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} @@ -67,7 +70,7 @@ class FormHelperActiveRecordTest < ActionView::TestCase end def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) - txt = %{<form accept-charset="UTF-8" action="#{action}"}.dup + txt = +%{<form accept-charset="UTF-8" action="#{action}"} txt << %{ enctype="multipart/form-data"} if multipart txt << %{ data-remote="true"} if remote txt << %{ class="#{html_class}"} if html_class diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index 4b931f793f..724129a7d9 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -62,10 +62,14 @@ module Weblog end class PolymorphicRoutesTest < ActionController::TestCase - include SharedTestRoutes.url_helpers + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw { } + include Routes.url_helpers + default_url_options[:host] = "example.com" def setup + super @project = Project.new @task = Task.new @step = Step.new @@ -763,9 +767,11 @@ class DirectRoutesTest < ActionView::TestCase include Routes.url_helpers def setup + super @category = Category.new("1") @collection = Collection.new("2") @product = Product.new("3") + @controller.singleton_class.include Routes.url_helpers end def test_direct_routes diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index bd0cd10eaf..a6befc3ee5 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -6,6 +6,7 @@ class RelationCacheTest < ActionView::TestCase tests ActionView::Helpers::CacheHelper def setup + super view_paths = ActionController::Base.view_paths lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) @view_renderer = ActionView::Renderer.new(lookup_context) @@ -19,5 +20,5 @@ class RelationCacheTest < ActionView::TestCase assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}") end - def view_cache_dependencies; end + def view_cache_dependencies; []; end end diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb index 3a698fa42e..2bb3cfeb5b 100644 --- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb @@ -3,6 +3,16 @@ require "active_record_unit" class RenderPartialWithRecordIdentificationController < ActionController::Base + ROUTES = test_routes do + get :render_with_record_collection, to: "render_partial_with_record_identification#render_with_record_collection" + get :render_with_scope, to: "render_partial_with_record_identification#render_with_scope" + get :render_with_record, to: "render_partial_with_record_identification#render_with_record" + get :render_with_has_many_association, to: "render_partial_with_record_identification#render_with_has_many_association" + get :render_with_has_many_and_belongs_to_association, to: "render_partial_with_record_identification#render_with_has_many_and_belongs_to_association" + get :render_with_has_one_association, to: "render_partial_with_record_identification#render_with_has_one_association" + get :render_with_record_collection_and_spacer_template, to: "render_partial_with_record_identification#render_with_record_collection_and_spacer_template" + end + def render_with_has_many_and_belongs_to_association @developer = Developer.find(1) render partial: @developer.projects @@ -89,6 +99,11 @@ end module Fun class NestedController < ActionController::Base + ROUTES = test_routes do + get :render_with_record_in_nested_controller, to: "fun/nested#render_with_record_in_nested_controller" + get :render_with_record_collection_in_nested_controller, to: "fun/nested#render_with_record_collection_in_nested_controller" + end + def render_with_record_in_nested_controller render partial: Game.new("Pong") end @@ -100,6 +115,11 @@ module Fun module Serious class NestedDeeperController < ActionController::Base + ROUTES = test_routes do + get :render_with_record_in_deeper_nested_controller, to: "fun/serious/nested_deeper#render_with_record_in_deeper_nested_controller" + get :render_with_record_collection_in_deeper_nested_controller, to: "fun/serious/nested_deeper#render_with_record_collection_in_deeper_nested_controller" + end + def render_with_record_in_deeper_nested_controller render partial: Game.new("Chess") end diff --git a/actionview/test/fixtures/ruby_template.ruby b/actionview/test/fixtures/ruby_template.ruby index 93334610a8..3e0bc445a2 100644 --- a/actionview/test/fixtures/ruby_template.ruby +++ b/actionview/test/fixtures/ruby_template.ruby @@ -1,2 +1,2 @@ -body = "".dup +body = +"" body << ["Hello", "from", "Ruby", "code"].join(" ") diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 131e49327e..e172497c88 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -40,7 +40,7 @@ class CaptureHelperTest < ActionView::TestCase assert_equal "<em>bar</em>", string end - def test_capture_used_for_read + def test_content_for_used_for_read content_for :foo, "foo" assert_equal "foo", content_for(:foo) @@ -219,7 +219,7 @@ class CaptureHelperTest < ActionView::TestCase def test_with_output_buffer_does_not_assume_there_is_an_output_buffer assert_nil @av.output_buffer - assert_equal "", @av.with_output_buffer {} + assert_equal "", @av.with_output_buffer { } end def alt_encoding(output_buffer) diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 701e6c86fd..0a294ec674 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -214,7 +214,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day - expected = %(<select id="date_day" name="date[day]">\n).dup + expected = +%(<select id="date_day" name="date[day]">\n) expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -223,7 +223,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_blank - expected = %(<select id="date_day" name="date[day]">\n).dup + expected = +%(<select id="date_day" name="date[day]">\n) expected << %(<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -232,7 +232,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_nil_with_blank - expected = %(<select id="date_day" name="date[day]">\n).dup + expected = +%(<select id="date_day" name="date[day]">\n) expected << %(<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -240,7 +240,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_two_digit_numbers - expected = %(<select id="date_day" name="date[day]">\n).dup + expected = +%(<select id="date_day" name="date[day]">\n) expected << %(<option value="1">01</option>\n<option selected="selected" value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -249,7 +249,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_html_options - expected = %(<select id="date_day" name="date[day]" class="selector">\n).dup + expected = +%(<select id="date_day" name="date[day]" class="selector">\n) expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -258,7 +258,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_default_prompt - expected = %(<select id="date_day" name="date[day]">\n).dup + expected = +%(<select id="date_day" name="date[day]">\n) expected << %(<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -266,7 +266,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_custom_prompt - expected = %(<select id="date_day" name="date[day]">\n).dup + expected = +%(<select id="date_day" name="date[day]">\n) expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -274,7 +274,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_generic_with_css_classes - expected = %(<select id="date_day" name="date[day]" class="day">\n).dup + expected = +%(<select id="date_day" name="date[day]" class="day">\n) expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -282,7 +282,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_day_with_custom_with_css_classes - expected = %(<select id="date_day" name="date[day]" class="my-day">\n).dup + expected = +%(<select id="date_day" name="date[day]" class="my-day">\n) expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" @@ -290,7 +290,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -299,7 +299,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_two_digit_numbers - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">01</option>\n<option value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8" selected="selected">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n) expected << "</select>\n" @@ -308,7 +308,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_disabled - expected = %(<select id="date_month" name="date[month]" disabled="disabled">\n).dup + expected = +%(<select id="date_month" name="date[month]" disabled="disabled">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -317,7 +317,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_field_name_override - expected = %(<select id="date_mois" name="date[mois]">\n).dup + expected = +%(<select id="date_mois" name="date[mois]">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -326,7 +326,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_blank - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value=""></option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -335,7 +335,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_nil_with_blank - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value=""></option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -343,7 +343,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_numbers - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8" selected="selected">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n) expected << "</select>\n" @@ -352,7 +352,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_numbers_and_names - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">1 - January</option>\n<option value="2">2 - February</option>\n<option value="3">3 - March</option>\n<option value="4">4 - April</option>\n<option value="5">5 - May</option>\n<option value="6">6 - June</option>\n<option value="7">7 - July</option>\n<option value="8" selected="selected">8 - August</option>\n<option value="9">9 - September</option>\n<option value="10">10 - October</option>\n<option value="11">11 - November</option>\n<option value="12">12 - December</option>\n) expected << "</select>\n" @@ -361,7 +361,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_format_string - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">January (01)</option>\n<option value="2">February (02)</option>\n<option value="3">March (03)</option>\n<option value="4">April (04)</option>\n<option value="5">May (05)</option>\n<option value="6">June (06)</option>\n<option value="7">July (07)</option>\n<option value="8" selected="selected">August (08)</option>\n<option value="9">September (09)</option>\n<option value="10">October (10)</option>\n<option value="11">November (11)</option>\n<option value="12">December (12)</option>\n) expected << "</select>\n" @@ -371,7 +371,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_numbers_and_names_with_abbv - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n) expected << "</select>\n" @@ -380,7 +380,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_abbv - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">Jan</option>\n<option value="2">Feb</option>\n<option value="3">Mar</option>\n<option value="4">Apr</option>\n<option value="5">May</option>\n<option value="6">Jun</option>\n<option value="7">Jul</option>\n<option value="8" selected="selected">Aug</option>\n<option value="9">Sep</option>\n<option value="10">Oct</option>\n<option value="11">Nov</option>\n<option value="12">Dec</option>\n) expected << "</select>\n" @@ -391,7 +391,7 @@ class DateHelperTest < ActionView::TestCase def test_select_month_with_custom_names month_names = %w(nil Januar Februar Marts April Maj Juni Juli August September Oktober November December) - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month]}</option>\n) } expected << "</select>\n" @@ -402,7 +402,7 @@ class DateHelperTest < ActionView::TestCase def test_select_month_with_zero_indexed_custom_names month_names = %w(Januar Februar Marts April Maj Juni Juli August September Oktober November December) - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month - 1]}</option>\n) } expected << "</select>\n" @@ -419,7 +419,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_html_options - expected = %(<select id="date_month" name="date[month]" class="selector" accesskey="M">\n).dup + expected = +%(<select id="date_month" name="date[month]" class="selector" accesskey="M">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -427,7 +427,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_default_prompt - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -435,7 +435,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_custom_prompt - expected = %(<select id="date_month" name="date[month]">\n).dup + expected = +%(<select id="date_month" name="date[month]">\n) expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -443,7 +443,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_generic_with_css_classes - expected = %(<select id="date_month" name="date[month]" class="month">\n).dup + expected = +%(<select id="date_month" name="date[month]" class="month">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -451,7 +451,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_month_with_custom_with_css_classes - expected = %(<select id="date_month" name="date[month]" class="my-month">\n).dup + expected = +%(<select id="date_month" name="date[month]" class="my-month">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -459,7 +459,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year - expected = %(<select id="date_year" name="date[year]">\n).dup + expected = +%(<select id="date_year" name="date[year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -468,7 +468,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_disabled - expected = %(<select id="date_year" name="date[year]" disabled="disabled">\n).dup + expected = +%(<select id="date_year" name="date[year]" disabled="disabled">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -477,7 +477,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_field_name_override - expected = %(<select id="date_annee" name="date[annee]">\n).dup + expected = +%(<select id="date_annee" name="date[annee]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -486,7 +486,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_type_discarding - expected = %(<select id="date_year" name="date_year">\n).dup + expected = +%(<select id="date_year" name="date_year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -497,7 +497,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_descending - expected = %(<select id="date_year" name="date[year]">\n).dup + expected = +%(<select id="date_year" name="date[year]">\n) expected << %(<option value="2005" selected="selected">2005</option>\n<option value="2004">2004</option>\n<option value="2003">2003</option>\n) expected << "</select>\n" @@ -514,7 +514,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_html_options - expected = %(<select id="date_year" name="date[year]" class="selector" accesskey="M">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="selector" accesskey="M">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -522,7 +522,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_default_prompt - expected = %(<select id="date_year" name="date[year]">\n).dup + expected = +%(<select id="date_year" name="date[year]">\n) expected << %(<option value="">Year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -530,7 +530,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_custom_prompt - expected = %(<select id="date_year" name="date[year]">\n).dup + expected = +%(<select id="date_year" name="date[year]">\n) expected << %(<option value="">Choose year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -538,7 +538,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_generic_with_css_classes - expected = %(<select id="date_year" name="date[year]" class="year">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="year">\n) expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -546,7 +546,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_custom_with_css_classes - expected = %(<select id="date_year" name="date[year]" class="my-year">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="my-year">\n) expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -554,7 +554,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_year_with_position - expected = %(<select id="date_year_1i" name="date[year(1i)]">\n).dup + expected = +%(<select id="date_year_1i" name="date[year(1i)]">\n) expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" assert_dom_equal expected, select_year(Date.current, include_position: true, start_year: 2003, end_year: 2005) @@ -570,7 +570,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour - expected = %(<select id="date_hour" name="date[hour]">\n).dup + expected = +%(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -578,7 +578,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_ampm - expected = %(<select id="date_hour" name="date[hour]">\n).dup + expected = +%(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n) expected << "</select>\n" @@ -586,7 +586,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_disabled - expected = %(<select id="date_hour" name="date[hour]" disabled="disabled">\n).dup + expected = +%(<select id="date_hour" name="date[hour]" disabled="disabled">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -594,7 +594,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_field_name_override - expected = %(<select id="date_heure" name="date[heure]">\n).dup + expected = +%(<select id="date_heure" name="date[heure]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -602,7 +602,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_blank - expected = %(<select id="date_hour" name="date[hour]">\n).dup + expected = +%(<select id="date_hour" name="date[hour]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -610,7 +610,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_nil_with_blank - expected = %(<select id="date_hour" name="date[hour]">\n).dup + expected = +%(<select id="date_hour" name="date[hour]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -618,7 +618,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_html_options - expected = %(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n).dup + expected = +%(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -626,7 +626,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_default_prompt - expected = %(<select id="date_hour" name="date[hour]">\n).dup + expected = +%(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -634,7 +634,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_custom_prompt - expected = %(<select id="date_hour" name="date[hour]">\n).dup + expected = +%(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -642,7 +642,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_generic_with_css_classes - expected = %(<select id="date_hour" name="date[hour]" class="hour">\n).dup + expected = +%(<select id="date_hour" name="date[hour]" class="hour">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -650,7 +650,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_hour_with_custom_with_css_classes - expected = %(<select id="date_hour" name="date[hour]" class="my-hour">\n).dup + expected = +%(<select id="date_hour" name="date[hour]" class="my-hour">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -658,7 +658,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -666,7 +666,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_disabled - expected = %(<select id="date_minute" name="date[minute]" disabled="disabled">\n).dup + expected = +%(<select id="date_minute" name="date[minute]" disabled="disabled">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -674,7 +674,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_field_name_override - expected = %(<select id="date_minuto" name="date[minuto]">\n).dup + expected = +%(<select id="date_minuto" name="date[minuto]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -682,7 +682,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_blank - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -690,7 +690,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_blank_and_step - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n) expected << "</select>\n" @@ -698,7 +698,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_nil_with_blank - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -706,7 +706,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_nil_with_blank_and_step - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n) expected << "</select>\n" @@ -722,7 +722,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_html_options - expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n).dup + expected = +%(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -730,7 +730,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_default_prompt - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -738,7 +738,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_custom_prompt - expected = %(<select id="date_minute" name="date[minute]">\n).dup + expected = +%(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -746,7 +746,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_generic_with_css_classes - expected = %(<select id="date_minute" name="date[minute]" class="minute">\n).dup + expected = +%(<select id="date_minute" name="date[minute]" class="minute">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -754,7 +754,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_custom_with_css_classes - expected = %(<select id="date_minute" name="date[minute]" class="my-minute">\n).dup + expected = +%(<select id="date_minute" name="date[minute]" class="my-minute">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -762,7 +762,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second - expected = %(<select id="date_second" name="date[second]">\n).dup + expected = +%(<select id="date_second" name="date[second]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -770,7 +770,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_disabled - expected = %(<select id="date_second" name="date[second]" disabled="disabled">\n).dup + expected = +%(<select id="date_second" name="date[second]" disabled="disabled">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -778,7 +778,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_field_name_override - expected = %(<select id="date_segundo" name="date[segundo]">\n).dup + expected = +%(<select id="date_segundo" name="date[segundo]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -786,7 +786,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_blank - expected = %(<select id="date_second" name="date[second]">\n).dup + expected = +%(<select id="date_second" name="date[second]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -794,7 +794,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_nil_with_blank - expected = %(<select id="date_second" name="date[second]">\n).dup + expected = +%(<select id="date_second" name="date[second]">\n) expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -802,7 +802,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_html_options - expected = %(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n).dup + expected = +%(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -810,7 +810,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_default_prompt - expected = %(<select id="date_second" name="date[second]">\n).dup + expected = +%(<select id="date_second" name="date[second]">\n) expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -818,7 +818,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_custom_prompt - expected = %(<select id="date_second" name="date[second]">\n).dup + expected = +%(<select id="date_second" name="date[second]">\n) expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -826,7 +826,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_generic_with_css_classes - expected = %(<select id="date_second" name="date[second]" class="second">\n).dup + expected = +%(<select id="date_second" name="date[second]" class="second">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -834,7 +834,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_second_with_custom_with_css_classes - expected = %(<select id="date_second" name="date[second]" class="my-second">\n).dup + expected = +%(<select id="date_second" name="date[second]" class="my-second">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -842,7 +842,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -867,7 +867,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_order - expected = %(<select id="date_first_month" name="date[first][month]">\n).dup + expected = +%(<select id="date_first_month" name="date[first][month]">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -884,7 +884,7 @@ class DateHelperTest < ActionView::TestCase def test_select_date_with_incomplete_order # Since the order is incomplete nothing will be shown - expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n).dup + expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="1" />\n) @@ -892,7 +892,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_disabled - expected = %(<select id="date_first_year" name="date[first][year]" disabled="disabled">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]" disabled="disabled">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -908,7 +908,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_no_start_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 1) do |y| if y == Date.today.year expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) @@ -932,7 +932,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_no_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) 2003.upto(2008) do |y| if y == 2003 expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) @@ -956,7 +956,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_no_start_or_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) do |y| if y == Date.today.year expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) @@ -980,7 +980,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_zero_value - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -996,7 +996,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_zero_value_and_no_start_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -1012,7 +1012,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_zero_value_and_no_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) last_year = Time.now.year + 5 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -1029,7 +1029,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_zero_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -1045,7 +1045,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -1061,7 +1061,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_html_options - expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]" class="selector">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1077,7 +1077,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_separator - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1097,7 +1097,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_separator_and_discard_day - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1113,7 +1113,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_separator_discard_month_and_day - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1124,7 +1124,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_hidden - expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n).dup + expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n) expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) @@ -1133,7 +1133,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_css_classes_option - expected = %(<select id="date_first_year" name="date[first][year]" class="year">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]" class="year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1149,7 +1149,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_custom_with_css_classes - expected = %(<select id="date_year" name="date[year]" class="my-year">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="my-year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1165,7 +1165,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_css_classes_option_and_html_class_option - expected = %(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1181,7 +1181,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_custom_with_css_classes_and_html_class_option - expected = %(<select id="date_year" name="date[year]" class="date optional my-year">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="date optional my-year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1197,7 +1197,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_partial_with_css_classes_and_html_class_option - expected = %(<select id="date_year" name="date[year]" class="date optional">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="date optional">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1213,7 +1213,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_html_class_option - expected = %(<select id="date_year" name="date[year]" class="date optional custom-grid">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="date optional custom-grid">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1229,7 +1229,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1257,7 +1257,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_ampm - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1285,7 +1285,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_separators - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1313,7 +1313,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -1341,7 +1341,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_html_options - expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]" class="selector">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1369,7 +1369,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_all_separators - expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]" class="selector">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1405,7 +1405,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_default_prompt - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="">Year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1434,7 +1434,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_custom_prompt - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1463,7 +1463,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_generic_with_css_classes - expected = %(<select id="date_year" name="date[year]" class="year">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1491,7 +1491,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_custom_with_css_classes - expected = %(<select id="date_year" name="date[year]" class="my-year">\n).dup + expected = +%(<select id="date_year" name="date[year]" class="my-year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1519,7 +1519,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_custom_hours - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1548,7 +1548,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_hidden - expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n).dup + expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) @@ -1560,7 +1560,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1579,7 +1579,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_ampm - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1597,7 +1597,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_separator - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) expected << %(<select id="date_hour" name="date[hour]">\n) @@ -1615,7 +1615,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_seconds - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1639,7 +1639,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_seconds_and_separator - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1663,7 +1663,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_html_options - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1686,7 +1686,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_default_prompt - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1710,7 +1710,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_custom_prompt - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1735,7 +1735,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_generic_with_css_classes - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1759,7 +1759,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_custom_with_css_classes - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n).dup + expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1783,7 +1783,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_hidden - expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n).dup + expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) @@ -1797,7 +1797,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -1817,7 +1817,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -1837,7 +1837,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -1875,7 +1875,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n".dup + expected = +"<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} @@ -1892,7 +1892,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 2, 29) - expected = "<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n".dup + expected = +"<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n" expected << "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} @@ -1906,7 +1906,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n".dup + expected = +"<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} @@ -1925,7 +1925,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = "<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n".dup + expected = +"<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n" expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} @@ -1946,7 +1946,7 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on) end - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} @@ -1962,7 +1962,7 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on) end - expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}.dup + expected = +%{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} @@ -1978,7 +1978,7 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on) end - expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}.dup + expected = +%{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} @@ -1990,7 +1990,7 @@ class DateHelperTest < ActionView::TestCase @post.written_on = Date.new(2004, 6, 15) id = 456 - expected = %{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n}.dup + expected = +%{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2010,7 +2010,7 @@ class DateHelperTest < ActionView::TestCase @post.written_on = Date.new(2004, 6, 15) id = 123 - expected = %{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n}.dup + expected = +%{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2029,7 +2029,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}.dup + expected = +%{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } expected << "</select>\n" @@ -2049,7 +2049,7 @@ class DateHelperTest < ActionView::TestCase start_year = Time.now.year - 5 end_year = Time.now.year + 5 - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} start_year.upto(end_year) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) } expected << "</select>\n" @@ -2069,7 +2069,7 @@ class DateHelperTest < ActionView::TestCase start_year = Time.now.year - 5 end_year = Time.now.year + 5 - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << "<option value=\"\"></option>\n" start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } expected << "</select>\n" @@ -2113,7 +2113,7 @@ class DateHelperTest < ActionView::TestCase start_year = Time.now.year - 5 end_year = Time.now.year + 5 - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << "<option value=\"\"></option>\n" start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } expected << "</select>\n" @@ -2145,7 +2145,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2164,7 +2164,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2188,7 +2188,7 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on, {}, { class: "selector" }) end - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2208,7 +2208,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2232,7 +2232,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}.dup + expected = +%{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} expected << "</select>\n" @@ -2255,7 +2255,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}.dup + expected = +%{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} expected << "</select>\n" @@ -2273,7 +2273,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2293,7 +2293,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2313,7 +2313,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2333,7 +2333,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2353,7 +2353,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2372,7 +2372,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2391,7 +2391,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="1" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="1" />\n} @@ -2410,7 +2410,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n).dup + expected = +%(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } expected << "</select>\n" expected << " : " @@ -2425,7 +2425,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2448,7 +2448,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2471,7 +2471,7 @@ class DateHelperTest < ActionView::TestCase concat f.time_select(:written_on, {}, { class: "selector" }) end - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2490,7 +2490,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2517,7 +2517,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2538,7 +2538,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2559,7 +2559,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2580,7 +2580,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} @@ -2601,7 +2601,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_written_on_3i" disabled="disabled" name="post[written_on(3i)]" value="15" />\n} @@ -2620,7 +2620,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2649,7 +2649,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2715,7 +2715,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2750,7 +2750,7 @@ class DateHelperTest < ActionView::TestCase concat f.datetime_select(:updated_at, {}, { class: "selector" }) end - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} expected << %{ — <select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option selected="selected" value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n} @@ -2763,7 +2763,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2816,7 +2816,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = nil - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2845,7 +2845,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = nil - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2874,7 +2874,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2903,7 +2903,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n}.dup + expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -2929,7 +2929,7 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_zero_value_and_no_start_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -2945,7 +2945,7 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_zero_value_and_no_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) last_year = Time.now.year + 5 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -2962,7 +2962,7 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_zero_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -2978,7 +2978,7 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -2994,7 +2994,7 @@ class DateHelperTest < ActionView::TestCase end def test_datetime_select_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n).dup + expected = +%(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" @@ -3026,7 +3026,7 @@ class DateHelperTest < ActionView::TestCase @post.updated_at = Time.local(2004, 6, 15, 16, 35) id = 456 - expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}.dup + expected = +%{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -3060,7 +3060,7 @@ class DateHelperTest < ActionView::TestCase concat f.datetime_select(:updated_at) end - expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}.dup + expected = +%{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -3090,7 +3090,7 @@ class DateHelperTest < ActionView::TestCase @post.updated_at = Time.local(2004, 6, 15, 16, 35) id = @post.id - expected = %{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}.dup + expected = +%{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" @@ -3119,7 +3119,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} @@ -3150,7 +3150,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n} expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } expected << "</select>\n" @@ -3175,7 +3175,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } expected << "</select>\n" expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n} @@ -3198,7 +3198,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n} @@ -3217,7 +3217,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n} expected << %{<input type="hidden" id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]" value="6" />\n} expected << %{<input type="hidden" id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]" value="1" />\n} @@ -3236,7 +3236,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} @@ -3253,7 +3253,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} @@ -3277,7 +3277,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n} 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]">\n} @@ -3301,7 +3301,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}.dup + expected = +%{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} @@ -3328,7 +3328,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) - expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}.dup + expected = +%{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n} expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } expected << "</select>\n" @@ -3353,7 +3353,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = nil - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} 2001.upto(2011) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2006}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} @@ -3380,7 +3380,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = nil - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} expected << %(<option value=""></option>\n) (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } expected << "</select>\n" @@ -3400,7 +3400,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = nil - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) } expected << "</select>\n" expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} @@ -3427,7 +3427,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) - expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n}.dup + expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" diff --git a/actionview/test/template/erb/form_for_test.rb b/actionview/test/template/erb/form_for_test.rb index b6ecf003a5..b3a47e17a4 100644 --- a/actionview/test/template/erb/form_for_test.rb +++ b/actionview/test/template/erb/form_for_test.rb @@ -6,7 +6,11 @@ require "template/erb/helper" module ERBTest class TagHelperTest < BlockTestCase test "form_for works" do - output = render_content "form_for(:staticpage, :url => {:controller => 'blah', :action => 'update'})", "" + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + get "/blah/update", to: "blah#update" + end + output = render_content "form_for(:staticpage, :url => {:controller => 'blah', :action => 'update'})", "", routes assert_match %r{<form.*action="/blah/update".*method="post">.*</form>}, output end end diff --git a/actionview/test/template/erb/helper.rb b/actionview/test/template/erb/helper.rb index 57d6cb1be3..727cc3dcf2 100644 --- a/actionview/test/template/erb/helper.rb +++ b/actionview/test/template/erb/helper.rb @@ -3,7 +3,6 @@ module ERBTest class ViewContext include ActionView::Helpers::UrlHelper - include SharedTestRoutes.url_helpers include ActionView::Helpers::TagHelper include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::FormHelper @@ -14,9 +13,15 @@ module ERBTest end class BlockTestCase < ActiveSupport::TestCase - def render_content(start, inside) + def render_content(start, inside, routes = nil) + routes ||= ActionDispatch::Routing::RouteSet.new.tap do |rs| + rs.draw { } + end + context = Class.new(ViewContext) { + include routes.url_helpers + }.new template = block_helper(start, inside) - ActionView::Template::Handlers::ERB.erb_implementation.new(template).evaluate(ViewContext.new) + ActionView::Template::Handlers::ERB.erb_implementation.new(template).evaluate(context) end def block_helper(str, rest) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index c30176349c..f84c9b2b73 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -37,7 +37,7 @@ class FormWithActsLikeFormTagTest < FormWithTest method = options[:method] skip_enforcing_utf8 = options.fetch(:skip_enforcing_utf8, false) - "".dup.tap do |txt| + (+"").tap do |txt| unless skip_enforcing_utf8 txt << %{<input name="utf8" type="hidden" value="✓" />} end @@ -53,7 +53,7 @@ class FormWithActsLikeFormTagTest < FormWithTest method = method.to_s == "get" ? "get" : "post" - txt = %{<form accept-charset="UTF-8" action="#{action}"}.dup + txt = +%{<form accept-charset="UTF-8" action="#{action}"} txt << %{ enctype="multipart/form-data"} if enctype txt << %{ data-remote="true"} unless local txt << %{ class="#{html_class}"} if html_class @@ -290,6 +290,7 @@ class FormWithActsLikeFormForTest < FormWithTest @post_delegator.title = "Hello World" @car = Car.new("#000FFF") + @controller.singleton_class.include Routes.url_helpers end Routes = ActionDispatch::Routing::RouteSet.new @@ -308,10 +309,6 @@ class FormWithActsLikeFormForTest < FormWithTest root to: "main#index" end - def _routes - Routes - end - include Routes.url_helpers def url_for(object) @@ -450,13 +447,15 @@ class FormWithActsLikeFormForTest < FormWithTest def test_form_with_doesnt_call_private_or_protected_properties_on_form_object_skipping_value obj = Class.new do - private def private_property - "That would be great." - end + private + def private_property + "That would be great." + end - protected def protected_property - "I believe you have my stapler." - end + protected + def protected_property + "I believe you have my stapler." + end end.new form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f| @@ -2247,7 +2246,7 @@ class FormWithActsLikeFormForTest < FormWithTest post.persisted = false def post.to_key; nil; end - form_with(model: post) {} + form_with(model: post) { } expected = whole_form("/posts") assert_dom_equal expected, output_buffer @@ -2255,14 +2254,14 @@ class FormWithActsLikeFormForTest < FormWithTest def test_form_with_with_existing_object_in_list @comment.save - form_with(model: [@post, @comment]) {} + form_with(model: [@post, @comment]) { } expected = whole_form(post_comment_path(@post, @comment), method: "patch") assert_dom_equal expected, output_buffer end def test_form_with_with_new_object_in_list - form_with(model: [@post, @comment]) {} + form_with(model: [@post, @comment]) { } expected = whole_form(post_comments_path(@post)) assert_dom_equal expected, output_buffer @@ -2270,14 +2269,14 @@ class FormWithActsLikeFormForTest < FormWithTest def test_form_with_with_existing_object_and_namespace_in_list @comment.save - form_with(model: [:admin, @post, @comment]) {} + form_with(model: [:admin, @post, @comment]) { } expected = whole_form(admin_post_comment_path(@post, @comment), method: "patch") assert_dom_equal expected, output_buffer end def test_form_with_with_new_object_and_namespace_in_list - form_with(model: [:admin, @post, @comment]) {} + form_with(model: [:admin, @post, @comment]) { } expected = whole_form(admin_post_comments_path(@post)) assert_dom_equal expected, output_buffer @@ -2291,13 +2290,13 @@ class FormWithActsLikeFormForTest < FormWithTest end def test_form_with_with_default_method_as_patch - form_with(model: @post) {} + form_with(model: @post) { } expected = whole_form("/posts/123", method: "patch") assert_dom_equal expected, output_buffer end def test_form_with_with_data_attributes - form_with(model: @post, data: { behavior: "stuff" }) {} + form_with(model: @post, data: { behavior: "stuff" }) { } assert_match %r|data-behavior="stuff"|, output_buffer assert_match %r|data-remote="true"|, output_buffer end @@ -2316,7 +2315,7 @@ class FormWithActsLikeFormForTest < FormWithTest end end - form_with(model: @post, builder: builder_class) {} + form_with(model: @post, builder: builder_class) { } assert_equal 1, initialization_count, "form builder instantiated more than once" end @@ -2325,9 +2324,9 @@ class FormWithActsLikeFormForTest < FormWithTest method = options[:method] if options.fetch(:skip_enforcing_utf8, false) - txt = "".dup + txt = +"" else - txt = %{<input name="utf8" type="hidden" value="✓" />}.dup + txt = +%{<input name="utf8" type="hidden" value="✓" />} end if method && !%w(get post).include?(method.to_s) @@ -2338,7 +2337,7 @@ class FormWithActsLikeFormForTest < FormWithTest end def form_text(action = "/", id = nil, html_class = nil, local = nil, multipart = nil, method = nil) - txt = %{<form accept-charset="UTF-8" action="#{action}"}.dup + txt = +%{<form accept-charset="UTF-8" action="#{action}"} txt << %{ enctype="multipart/form-data"} if multipart txt << %{ data-remote="true"} unless local txt << %{ class="#{html_class}"} if html_class diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 38bb1e1d24..5972946074 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -140,6 +140,7 @@ class FormHelperTest < ActionView::TestCase @post_delegator.title = "Hello World" @car = Car.new("#000FFF") + @controller.singleton_class.include Routes.url_helpers end Routes = ActionDispatch::Routing::RouteSet.new @@ -3487,14 +3488,14 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_existing_object_in_list @comment.save - form_for([@post, @comment]) {} + form_for([@post, @comment]) { } expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_in_list - form_for([@post, @comment]) {} + form_for([@post, @comment]) { } expected = whole_form(post_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer @@ -3502,14 +3503,14 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_existing_object_and_namespace_in_list @comment.save - form_for([:admin, @post, @comment]) {} + form_for([:admin, @post, @comment]) { } expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_and_namespace_in_list - form_for([:admin, @post, @comment]) {} + form_for([:admin, @post, @comment]) { } expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer @@ -3523,13 +3524,13 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_default_method_as_patch - form_for(@post) {} + form_for(@post) { } expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") assert_dom_equal expected, output_buffer end def test_form_for_with_data_attributes - form_for(@post, data: { behavior: "stuff" }, remote: true) {} + form_for(@post, data: { behavior: "stuff" }, remote: true) { } assert_match %r|data-behavior="stuff"|, output_buffer assert_match %r|data-remote="true"|, output_buffer end @@ -3548,7 +3549,7 @@ class FormHelperTest < ActionView::TestCase end end - form_for(@post, builder: builder_class) {} + form_for(@post, builder: builder_class) { } assert_equal 1, initialization_count, "form builder instantiated more than once" end @@ -3558,9 +3559,9 @@ class FormHelperTest < ActionView::TestCase method = options[:method] if options.fetch(:enforce_utf8, true) - txt = %{<input name="utf8" type="hidden" value="✓" />}.dup + txt = +%{<input name="utf8" type="hidden" value="✓" />} else - txt = "".dup + txt = +"" end if method && !%w(get post).include?(method.to_s) @@ -3571,7 +3572,7 @@ class FormHelperTest < ActionView::TestCase end def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) - txt = %{<form accept-charset="UTF-8" action="#{action}"}.dup + txt = +%{<form accept-charset="UTF-8" action="#{action}"} txt << %{ enctype="multipart/form-data"} if multipart txt << %{ data-remote="true"} if remote txt << %{ class="#{html_class}"} if html_class diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index 8f796bdb83..a2d1474a94 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -21,7 +21,12 @@ class FormOptionsHelperTest < ActionView::TestCase tests ActionView::Helpers::FormOptionsHelper silence_warnings do - Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments) + Post = Struct.new("Post", :title, :author_name, :body, :written_on, :category, :origin, :allow_comments) do + private + def secret + "This is super secret: #{author_name} is not the real author of #{title}" + end + end Continent = Struct.new("Continent", :continent_name, :countries) Country = Struct.new("Country", :country_id, :country_name) Firm = Struct.new("Firm", :time_zone) @@ -68,6 +73,14 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_collection_options_with_private_value_method + assert_deprecated("Using private methods from view helpers is deprecated (calling private Struct::Post#secret)") { options_from_collection_for_select(dummy_posts, "secret", "title") } + end + + def test_collection_options_with_private_text_method + assert_deprecated("Using private methods from view helpers is deprecated (calling private Struct::Post#secret)") { options_from_collection_for_select(dummy_posts, "author_name", "secret") } + end + def test_collection_options_with_preselected_value assert_dom_equal( "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", @@ -655,7 +668,7 @@ class FormOptionsHelperTest < ActionView::TestCase @post = Post.new output_buffer = fields_for :post, @post do |f| - concat(f.select(:category) {}) + concat(f.select(:category) { }) end assert_dom_equal( diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index a3500a7eb3..9ece9f3ad1 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -26,7 +26,7 @@ class FormTagHelperTest < ActionView::TestCase method = options[:method] enforce_utf8 = options.fetch(:enforce_utf8, true) - "".dup.tap do |txt| + (+"").tap do |txt| if enforce_utf8 txt << %{<input name="utf8" type="hidden" value="✓" />} end @@ -42,7 +42,7 @@ class FormTagHelperTest < ActionView::TestCase method = method.to_s == "get" ? "get" : "post" - txt = %{<form accept-charset="UTF-8" action="#{action}"}.dup + txt = +%{<form accept-charset="UTF-8" action="#{action}"} txt << %{ enctype="multipart/form-data"} if enctype txt << %{ data-remote="true"} if remote txt << %{ class="#{html_class}"} if html_class diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb index a72bc6c2fe..4c28aeaee1 100644 --- a/actionview/test/template/javascript_helper_test.rb +++ b/actionview/test/template/javascript_helper_test.rb @@ -23,11 +23,15 @@ class JavaScriptHelperTest < ActionView::TestCase def test_escape_javascript assert_equal "", escape_javascript(nil) + assert_equal "123", escape_javascript(123) + assert_equal "en", escape_javascript(:en) + assert_equal "false", escape_javascript(false) + assert_equal "true", escape_javascript(true) assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos')) assert_equal %(backslash\\\\test), escape_javascript(%(backslash\\test)) assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags)) - assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\250 newline).dup.force_encoding(Encoding::UTF_8).encode!) - assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\251 newline).dup.force_encoding(Encoding::UTF_8).encode!) + assert_equal %(unicode 
 newline), escape_javascript((+%(unicode \342\200\250 newline)).force_encoding(Encoding::UTF_8).encode!) + assert_equal %(unicode 
 newline), escape_javascript((+%(unicode \342\200\251 newline)).force_encoding(Encoding::UTF_8).encode!) assert_equal %(dont <\\/close> tags), j(%(dont </close> tags)) end diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 7f4fd25573..9fcf80bb24 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -129,14 +129,14 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait *, cached_inner, uncached_outer = @logger.logged(:info) assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner) - assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer) # Second render hits the cache for the _cached_customer partial. Outer template's log shouldn't be affected. @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) wait *, cached_inner, uncached_outer = @logger.logged(:info) assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, cached_inner) - assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer) end end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 8782eb4430..afe68b7ff0 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -10,7 +10,7 @@ module RenderTestCases def setup_view(paths) @assigns = { secret: "in the sauce" } @view = Class.new(ActionView::Base) do - def view_cache_dependencies; end + def view_cache_dependencies; []; end def combined_fragment_cache_key(key) [ :views, key ] @@ -585,7 +585,7 @@ module RenderTestCases def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler - assert_equal @view.render(inline: "Hello, World!".dup, type: :foo1), @view.render(inline: "Hello, World!".dup, type: :foo2) + assert_equal @view.render(inline: +"Hello, World!", type: :foo1), @view.render(inline: +"Hello, World!", type: :foo2) ensure ActionView::Template.unregister_template_handler :foo1, :foo2 end diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb index ef000300cc..f196c42c4f 100644 --- a/actionview/test/template/streaming_render_test.rb +++ b/actionview/test/template/streaming_render_test.rb @@ -19,7 +19,7 @@ class SetupFiberedBase < ActiveSupport::TestCase def buffered_render(options) body = render_body(options) - string = "".dup + string = +"" body.each do |piece| string << piece end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index 3dc14e36e0..b348d1f17b 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -196,6 +196,13 @@ class TestERBTemplate < ActiveSupport::TestCase assert_match(Regexp.new("\xFC"), e.message) end + def test_template_is_marshalable + template = new_template + serialized = Marshal.load(Marshal.dump(template)) + assert_equal template.identifier, serialized.identifier + assert_equal template.source, serialized.source + end + def with_external_encoding(encoding) old = Encoding.default_external Encoding::Converter.new old, encoding if old != encoding diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index d98fd4f9a2..976b6bc77e 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -217,8 +217,14 @@ module ActionView test "is able to use routes" do controller.request.assign_parameters(@routes, "foo", "index", {}, "/foo", []) - assert_equal "/foo", url_for - assert_equal "/bar", url_for(controller: "bar") + with_routing do |set| + set.draw { + get :foo, to: "foo#index" + get :bar, to: "bar#index" + } + assert_equal "/foo", url_for + assert_equal "/bar", url_for(controller: "bar") + end end test "is able to use named routes" do @@ -236,7 +242,7 @@ module ActionView @routes ||= ActionDispatch::Routing::RouteSet.new end - routes.draw { get "bar", to: lambda {} } + routes.draw { get "bar", to: lambda { } } def self.call(*) end @@ -244,6 +250,8 @@ module ActionView set.draw { mount app => "/foo", :as => "foo_app" } + singleton_class.include set.mounted_helpers + assert_equal "/foo/bar", foo_app.bar_path end end diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index 45edfe18be..2925489f5d 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -9,11 +9,11 @@ class TextHelperTest < ActionView::TestCase super # This simulates the fact that instance variables are reset every time # a view is rendered. The cycle helper depends on this behavior. - @_cycles = nil if (defined? @_cycles) + @_cycles = nil if defined?(@_cycles) end def test_concat - self.output_buffer = "foo".dup + self.output_buffer = +"foo" assert_equal "foobar", concat("bar") assert_equal "foobar", output_buffer end @@ -34,10 +34,10 @@ class TextHelperTest < ActionView::TestCase assert_equal "<p>A paragraph</p>\n\n<p>and another one!</p>", simple_format("A paragraph\n\nand another one!") assert_equal "<p>A paragraph\n<br /> With a newline</p>", simple_format("A paragraph\n With a newline") - text = "A\nB\nC\nD".freeze + text = "A\nB\nC\nD" assert_equal "<p>A\n<br />B\n<br />C\n<br />D</p>", simple_format(text) - text = "A\r\n \nB\n\n\r\n\t\nC\nD".freeze + text = "A\r\n \nB\n\n\r\n\t\nC\nD" assert_equal "<p>A\n<br /> \n<br />B</p>\n\n<p>\t\n<br />C\n<br />D</p>", simple_format(text) assert_equal '<p class="test">This is a classy test</p>', simple_format("This is a classy test", class: "test") @@ -106,8 +106,8 @@ class TextHelperTest < ActionView::TestCase end def test_truncate_multibyte - assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".dup.force_encoding(Encoding::UTF_8), - truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".dup.force_encoding(Encoding::UTF_8), length: 10) + assert_equal (+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...").force_encoding(Encoding::UTF_8), + truncate((+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244").force_encoding(Encoding::UTF_8), length: 10) end def test_truncate_does_not_modify_the_options_hash @@ -327,7 +327,7 @@ class TextHelperTest < ActionView::TestCase end def test_excerpt_with_utf8 - assert_equal("...\357\254\203ciency could not be...".dup.force_encoding(Encoding::UTF_8), excerpt("That's why e\357\254\203ciency could not be helped".dup.force_encoding(Encoding::UTF_8), "could", radius: 8)) + assert_equal((+"...\357\254\203ciency could not be...").force_encoding(Encoding::UTF_8), excerpt((+"That's why e\357\254\203ciency could not be helped").force_encoding(Encoding::UTF_8), "could", radius: 8)) end def test_excerpt_does_not_modify_the_options_hash diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 9d91dbb72b..1ab28e4749 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -75,6 +75,15 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "javascript:history.back()", url_for(:back) end + def test_url_for_with_array_defaults_to_only_path_true + assert_equal "/other", url_for([:other, { controller: "foo" }]) + end + + def test_url_for_with_array_and_only_path_set_to_false + default_url_options[:host] = "http://example.com" + assert_equal "http://example.com/other", url_for([:other, { controller: "foo", only_path: false }]) + end + def test_to_form_params_with_hash assert_equal( [{ name: "name", value: "David" }, { name: "nationality", value: "Danish" }], @@ -548,7 +557,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_current_page_with_escaped_params_with_different_encoding @request = request_for_url("/") - @request.stub(:path, "/category/administra%c3%a7%c3%a3o".dup.force_encoding(Encoding::ASCII_8BIT)) do + @request.stub(:path, (+"/category/administra%c3%a7%c3%a3o").force_encoding(Encoding::ASCII_8BIT)) do assert current_page?(controller: "foo", action: "category", category: "administração") assert current_page?("http://www.example.com/category/administra%c3%a7%c3%a3o") end @@ -704,7 +713,7 @@ end class UrlHelperControllerTest < ActionController::TestCase class UrlHelperController < ActionController::Base - test_routes do + ROUTES = test_routes do get "url_helper_controller_test/url_helper/show/:id", to: "url_helper_controller_test/url_helper#show", as: :show @@ -768,6 +777,11 @@ class UrlHelperControllerTest < ActionController::TestCase helper_method :override_url_helper_path end + def setup + super + @routes = UrlHelperController::ROUTES + end + tests UrlHelperController def test_url_for_shows_only_path @@ -828,7 +842,7 @@ class UrlHelperControllerTest < ActionController::TestCase end class TasksController < ActionController::Base - test_routes do + ROUTES = test_routes do resources :tasks end @@ -850,6 +864,11 @@ end class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase tests TasksController + def setup + super + @routes = TasksController::ROUTES + end + def test_link_to_unless_current_to_current get :index assert_equal "tasks\ntasks", @response.body @@ -882,7 +901,7 @@ class Session end class WorkshopsController < ActionController::Base - test_routes do + ROUTES = test_routes do resources :workshops do resources :sessions end @@ -905,7 +924,7 @@ class WorkshopsController < ActionController::Base end class SessionsController < ActionController::Base - test_routes do + ROUTES = test_routes do resources :workshops do resources :sessions end @@ -932,6 +951,11 @@ class SessionsController < ActionController::Base end class PolymorphicControllerTest < ActionController::TestCase + def setup + super + @routes = WorkshopsController::ROUTES + end + def test_new_resource @controller = WorkshopsController.new @@ -946,6 +970,20 @@ class PolymorphicControllerTest < ActionController::TestCase assert_equal %{/workshops/1\n<a href="/workshops/1">Workshop</a>}, @response.body end + def test_current_page_when_options_does_not_respond_to_to_hash + @controller = WorkshopsController.new + + get :edit, params: { id: 1 } + assert_equal "false", @response.body + end +end + +class PolymorphicSessionsControllerTest < ActionController::TestCase + def setup + super + @routes = SessionsController::ROUTES + end + def test_new_nested_resource @controller = SessionsController.new @@ -966,11 +1004,4 @@ class PolymorphicControllerTest < ActionController::TestCase get :edit, params: { workshop_id: 1, id: 1, format: "json" } assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body end - - def test_current_page_when_options_does_not_respond_to_to_hash - @controller = WorkshopsController.new - - get :edit, params: { id: 1 } - assert_equal "false", @response.body - end end diff --git a/actionview/test/ujs/public/test/data-disable.js b/actionview/test/ujs/public/test/data-disable.js index e9919764b6..88dc801b2f 100644 --- a/actionview/test/ujs/public/test/data-disable.js +++ b/actionview/test/ujs/public/test/data-disable.js @@ -320,3 +320,20 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:error` event start() }, 30) }) + +asyncTest('do not enable elements for XHR redirects', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/echo?with_xhr_redirect=true') + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:send', function() { + App.checkDisabledState(link, 'Click me') + }) + .triggerNative('click') + + setTimeout(function() { + App.checkDisabledState(link, 'Click me') + start() + }, 30) +}) diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js index b1ce3b8c64..05677f2595 100644 --- a/actionview/test/ujs/public/test/settings.js +++ b/actionview/test/ujs/public/test/settings.js @@ -1,4 +1,5 @@ var App = App || {} +var Turbolinks = Turbolinks || {} App.assertCallbackInvoked = function(callbackName) { ok(true, callbackName + ' callback should have been invoked') @@ -116,3 +117,6 @@ $.fn.extend({ return this } }) + +Turbolinks.clearCache = function() {} +Turbolinks.visit = function() {} diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb index 48e9bcb65f..56f436c8b8 100644 --- a/actionview/test/ujs/server.rb +++ b/actionview/test/ujs/server.rb @@ -64,7 +64,12 @@ class TestsController < ActionController::Base if params[:content_type] && params[:content] render inline: params[:content], content_type: params[:content_type] elsif request.xhr? - render json: JSON.generate(data) + if params[:with_xhr_redirect] + response.set_header("X-Xhr-Redirect", "http://example.com/") + render inline: %{Turbolinks.clearCache()\nTurbolinks.visit("http://example.com/", {"action":"replace"})} + else + render json: JSON.generate(data) + end elsif params[:iframe] payload = JSON.generate(data).gsub("<", "<").gsub(">", ">") html = <<-HTML diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 8526741383..8bbecd5a5a 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,23 +1,52 @@ -* Move `enqueue`/`enqueue_at` notifications to an around callback. +* Restore HashWithIndifferentAccess support to ActiveJob::Arguments.deserialize. - Improves timing accuracy over the old after callback by including - time spent writing to the adapter's IO implementation. + *Gannon McGibbon* - *Zach Kemp* +* Include deserialized arguments in job instances returned from + `assert_enqueued_with` and `assert_performed_with` -* Allow `queue` option to `assert_no_enqueued_jobs`. + *Alan Wu* - Example: - ``` - def test_no_logging - assert_no_enqueued_jobs queue: 'default' do - LoggingJob.set(queue: :some_queue).perform_later - end - end - ``` +* Allow `assert_enqueued_with`/`assert_performed_with` methods to accept + a proc for the `args` argument. This is useful to check if only a subset of arguments + matches your expectations. + + *Edouard Chin* + +* `ActionDispatch::IntegrationTest` includes `ActiveJob::TestHelper` module by default. + + *Ricardo Díaz* + +* Added `enqueue_retry.active_job`, `retry_stopped.active_job`, and `discard.active_job` hooks. + + *steves* + +* Allow `assert_performed_with` to be called without a block. + + *bogdanvlviv* + +* Execution of `assert_performed_jobs`, and `assert_no_performed_jobs` + without a block should respect passed `:except`, `:only`, and `:queue` options. + + *bogdanvlviv* + +* Allow `:queue` option to job assertions and helpers. *bogdanvlviv* +* Allow `perform_enqueued_jobs` to be called without a block. + + Performs all of the jobs that have been enqueued up to this point in the test. + + *Kevin Deisz* + +* Move `enqueue`/`enqueue_at` notifications to an around callback. + + Improves timing accuracy over the old after callback by including + time spent writing to the adapter's IO implementation. + + *Zach Kemp* + * Allow call `assert_enqueued_with` with no block. Example: diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index be6292f737..cd7ac210ec 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -25,6 +25,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activejob/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "globalid", ">= 0.3.6" end diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 86bb0c5540..a86a82c6e3 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -24,18 +24,20 @@ module ActiveJob module Arguments extend self # :nodoc: - TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] + PERMITTED_TYPES = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] - # Serializes a set of arguments. Whitelisted types are returned - # as-is. Arrays/Hashes are serialized element by element. - # All other types are serialized using GlobalID. + # Serializes a set of arguments. Intrinsic types that can safely be + # serialized without mutation are returned as-is. Arrays/Hashes are + # serialized element by element. All other types are serialized using + # GlobalID. def serialize(arguments) arguments.map { |argument| serialize_argument(argument) } end - # Deserializes a set of arguments. Whitelisted types are returned - # as-is. Arrays/Hashes are deserialized element by element. - # All other types are deserialized using GlobalID. + # Deserializes a set of arguments. Intrinsic types that can safely be + # deserialized without mutation are returned as-is. Arrays/Hashes are + # deserialized element by element. All other types are deserialized using + # GlobalID. def deserialize(arguments) arguments.map { |argument| deserialize_argument(argument) } rescue @@ -45,11 +47,11 @@ module ActiveJob private # :nodoc: - GLOBALID_KEY = "_aj_globalid".freeze + GLOBALID_KEY = "_aj_globalid" # :nodoc: - SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze + SYMBOL_KEYS_KEY = "_aj_symbol_keys" # :nodoc: - WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze + WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access" # :nodoc: OBJECT_SERIALIZER_KEY = "_aj_serialized" @@ -60,11 +62,11 @@ module ActiveJob OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym, WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym, ] - private_constant :RESERVED_KEYS + private_constant :RESERVED_KEYS, :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY def serialize_argument(argument) case argument - when *TYPE_WHITELIST + when *PERMITTED_TYPES argument when GlobalID::Identification convert_to_global_id_hash(argument) @@ -88,7 +90,7 @@ module ActiveJob case argument when String GlobalID::Locator.locate(argument) || argument - when *TYPE_WHITELIST + when *PERMITTED_TYPES argument when Array argument.map { |arg| deserialize_argument(arg) } @@ -145,7 +147,10 @@ module ActiveJob end def transform_symbol_keys(hash, symbol_keys) - hash.transform_keys do |key| + # NOTE: HashWithIndifferentAccess#transform_keys always + # returns stringified keys with indifferent access + # so we call #to_h here to ensure keys are symbolized. + hash.to_h.transform_keys do |key| if symbol_keys.include?(key) key.to_sym else diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 61d402cfca..62bb5861bb 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -6,35 +6,33 @@ module ActiveJob module Core extend ActiveSupport::Concern - included do - # Job arguments - attr_accessor :arguments - attr_writer :serialized_arguments + # Job arguments + attr_accessor :arguments + attr_writer :serialized_arguments - # Timestamp when the job should be performed - attr_accessor :scheduled_at + # Timestamp when the job should be performed + attr_accessor :scheduled_at - # Job Identifier - attr_accessor :job_id + # Job Identifier + attr_accessor :job_id - # Queue in which the job will reside. - attr_writer :queue_name + # Queue in which the job will reside. + attr_writer :queue_name - # Priority that the job will have (lower is more priority). - attr_writer :priority + # Priority that the job will have (lower is more priority). + attr_writer :priority - # ID optionally provided by adapter - attr_accessor :provider_job_id + # ID optionally provided by adapter + attr_accessor :provider_job_id - # Number of times this job has been executed (which increments on every retry, like after an exception). - attr_accessor :executions + # Number of times this job has been executed (which increments on every retry, like after an exception). + attr_accessor :executions - # I18n.locale to be used during the job. - attr_accessor :locale + # I18n.locale to be used during the job. + attr_accessor :locale - # Timezone to be used during the job. - attr_accessor :timezone - end + # Timezone to be used during the job. + attr_accessor :timezone # These methods will be included into any Active Job object, adding # helpers for de/serialization and creation of job instances. diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index 1e57dbcb1c..bc9e168971 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -9,6 +9,7 @@ module ActiveJob module ClassMethods # Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts. + # The number of attempts includes the total executions of a job, not just the retried executions. # If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to # bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a # holding queue for inspection. @@ -21,7 +22,8 @@ module ActiveJob # as a computing proc that the number of executions so far as an argument, or as a symbol reference of # <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt> # (first wait 3s, then 18s, then 83s, etc) - # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts) + # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts), + # attempts here refers to the total number of times the job is executed, not just retried executions # * <tt>:queue</tt> - Re-enqueues the job on a different queue # * <tt>:priority</tt> - Re-enqueues the job with a different priority # @@ -45,13 +47,14 @@ module ActiveJob def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil) rescue_from(*exceptions) do |error| if executions < attempts - logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{error.class}. The original exception was #{error.cause.inspect}." - retry_job wait: determine_delay(wait), queue: queue, priority: priority + retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error else if block_given? - yield self, error + instrument :retry_stopped, error: error do + yield self, error + end else - logger.error "Stopped retrying #{self.class} due to a #{error.class}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}." + instrument :retry_stopped, error: error raise error end end @@ -78,10 +81,8 @@ module ActiveJob # end def discard_on(*exceptions) rescue_from(*exceptions) do |error| - if block_given? - yield self, error - else - logger.error "Discarded #{self.class} due to a #{error.class}. The original exception was #{error.cause.inspect}." + instrument :discard, error: error do + yield self, error if block_given? end end end @@ -109,7 +110,9 @@ module ActiveJob # end # end def retry_job(options = {}) - enqueue options + instrument :enqueue_retry, options.slice(:error, :wait) do + enqueue options + end end private @@ -130,5 +133,11 @@ module ActiveJob raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}" end end + + def instrument(name, error: nil, wait: nil, &block) + payload = { job: self, adapter: self.class.queue_adapter, error: error, wait: wait } + + ActiveSupport::Notifications.instrument("#{name}.active_job", payload, &block) + end end end diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb index d75be376ec..f5a343311f 100644 --- a/activejob/lib/active_job/execution.rb +++ b/activejob/lib/active_job/execution.rb @@ -31,11 +31,11 @@ module ActiveJob # # MyJob.new(*args).perform_now def perform_now + # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters + self.executions = (executions || 0) + 1 + deserialize_arguments_if_needed run_callbacks :perform do - # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters - self.executions = (executions || 0) + 1 - perform(*arguments) end rescue => exception diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 9ffd60ad53..416be83c24 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -88,6 +88,38 @@ module ActiveJob end end + def enqueue_retry(event) + job = event.payload[:job] + ex = event.payload[:error] + wait = event.payload[:wait] + + info do + if ex + "Retrying #{job.class} in #{wait.to_i} seconds, due to a #{ex.class}." + else + "Retrying #{job.class} in #{wait.to_i} seconds." + end + end + end + + def retry_stopped(event) + job = event.payload[:job] + ex = event.payload[:error] + + error do + "Stopped retrying #{job.class} due to a #{ex.class}, which reoccurred on #{job.executions} attempts." + end + end + + def discard(event) + job = event.payload[:job] + ex = event.payload[:error] + + error do + "Discarded #{job.class} due to a #{ex.class}." + end + end + private def queue_name(event) event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})" diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index 006a683b85..954bfd1dd1 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -22,6 +22,8 @@ module ActiveJob _queue_adapter end + # Returns string denoting the name of the configured queue adapter. + # By default returns +"async"+. def queue_adapter_name _queue_adapter_name end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index 00c7b407b1..3e3a474fbb 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -121,7 +121,7 @@ module ActiveJob autoload :SuckerPunchAdapter autoload :TestAdapter - ADAPTER = "Adapter".freeze + ADAPTER = "Adapter" private_constant :ADAPTER class << self diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb index ebf6f384e3..53a7e3d53e 100644 --- a/activejob/lib/active_job/queue_adapters/async_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb @@ -31,7 +31,7 @@ module ActiveJob # jobs. Since jobs share a single thread pool, long-running jobs will block # short-lived jobs. Fine for dev/test; bad for production. class AsyncAdapter - # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options. + # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options. def initialize(**executor_options) @scheduler = Scheduler.new(**executor_options) end diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb index 0ba93c6e0b..7dc49310ac 100644 --- a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb @@ -16,12 +16,12 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :backburner class BackburnerAdapter def enqueue(job) #:nodoc: - Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name + Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority) end def enqueue_at(job, timestamp) #:nodoc: delay = timestamp - Time.current.to_f - Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay + Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay) end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb index 885f9ff01c..f73ad444ba 100644 --- a/activejob/lib/active_job/queue_adapters/test_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb @@ -12,7 +12,7 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :test class TestAdapter - attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject) + attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject, :queue) attr_writer(:enqueued_jobs, :performed_jobs) # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. @@ -29,14 +29,14 @@ module ActiveJob return if filtered?(job) job_data = job_to_hash(job) - enqueue_or_perform(perform_enqueued_jobs, job, job_data) + perform_or_enqueue(perform_enqueued_jobs, job, job_data) end def enqueue_at(job, timestamp) #:nodoc: return if filtered?(job) job_data = job_to_hash(job, at: timestamp) - enqueue_or_perform(perform_enqueued_at_jobs, job, job_data) + perform_or_enqueue(perform_enqueued_at_jobs, job, job_data) end private @@ -44,7 +44,7 @@ module ActiveJob { job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras) end - def enqueue_or_perform(perform, job, job_data) + def perform_or_enqueue(perform, job, job_data) if perform performed_jobs << job_data Base.execute job.serialize @@ -54,12 +54,20 @@ module ActiveJob end def filtered?(job) + filtered_queue?(job) || filtered_job_class?(job) + end + + def filtered_queue?(job) + if queue + job.queue_name != queue.to_s + end + end + + def filtered_job_class?(job) if filter !Array(filter).include?(job.class) elsif reject Array(reject).include?(job.class) - else - false end end end diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb index d0294854d3..ecc0908d5f 100644 --- a/activejob/lib/active_job/railtie.rb +++ b/activejob/lib/active_job/railtie.rb @@ -30,6 +30,10 @@ module ActiveJob send(k, v) if respond_to? k end end + + ActiveSupport.on_load(:action_dispatch_integration_test) do + include ActiveJob::TestHelper + end end initializer "active_job.set_reloader_hook" do |app| diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 04cde28a96..0deb68d0d2 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -52,7 +52,7 @@ module ActiveJob queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter } end - # Specifies the queue adapter to use with all active job test helpers. + # Specifies the queue adapter to use with all Active Job test helpers. # # Returns an instance of the queue adapter and defaults to # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>. @@ -75,7 +75,7 @@ module ActiveJob # assert_enqueued_jobs 2 # end # - # If a block is passed, that block will cause the specified number of + # If a block is passed, asserts that the block will cause the specified number of # jobs to be enqueued. # # def test_jobs_again @@ -89,7 +89,7 @@ module ActiveJob # end # end # - # The number of times a specific job was enqueued can be asserted. + # Asserts the number of times a specific job was enqueued by passing +:only+ option. # # def test_logging_job # assert_enqueued_jobs 1, only: LoggingJob do @@ -98,7 +98,7 @@ module ActiveJob # end # end # - # The number of times a job except specific class was enqueued can be asserted. + # Asserts the number of times a job except specific class was enqueued by passing +:except+ option. # # def test_logging_job # assert_enqueued_jobs 1, except: HelloJob do @@ -107,7 +107,7 @@ module ActiveJob # end # end # - # The number of times a job is enqueued to a specific queue can also be asserted. + # Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option. # # def test_logging_job # assert_enqueued_jobs 2, queue: 'default' do @@ -117,14 +117,18 @@ module ActiveJob # end def assert_enqueued_jobs(number, only: nil, except: nil, queue: nil) if block_given? - original_count = enqueued_jobs_size(only: only, except: except, queue: queue) + original_count = enqueued_jobs_with(only: only, except: except, queue: queue) + yield - new_count = enqueued_jobs_size(only: only, except: except, queue: queue) - assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" + + new_count = enqueued_jobs_with(only: only, except: except, queue: queue) + + actual_count = new_count - original_count else - actual_count = enqueued_jobs_size(only: only, except: except, queue: queue) - assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" + actual_count = enqueued_jobs_with(only: only, except: except, queue: queue) end + + assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" end # Asserts that no jobs have been enqueued. @@ -135,7 +139,7 @@ module ActiveJob # assert_enqueued_jobs 1 # end # - # If a block is passed, that block should not cause any job to be enqueued. + # If a block is passed, asserts that the block will not cause any job to be enqueued. # # def test_jobs_again # assert_no_enqueued_jobs do @@ -143,7 +147,7 @@ module ActiveJob # end # end # - # It can be asserted that no jobs of a specific kind are enqueued: + # Asserts that no jobs of a specific kind are enqueued by passing +:only+ option. # # def test_no_logging # assert_no_enqueued_jobs only: LoggingJob do @@ -151,7 +155,7 @@ module ActiveJob # end # end # - # It can be asserted that no jobs except specific class are enqueued: + # Asserts that no jobs except specific class are enqueued by passing +:except+ option. # # def test_no_logging # assert_no_enqueued_jobs except: HelloJob do @@ -159,7 +163,7 @@ module ActiveJob # end # end # - # It can be asserted that no jobs are enqueued to a specific queue: + # Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option # # def test_no_logging # assert_no_enqueued_jobs queue: 'default' do @@ -176,7 +180,7 @@ module ActiveJob # Asserts that the number of performed jobs matches the given number. # If no block is passed, <tt>perform_enqueued_jobs</tt> - # must be called around the job call. + # must be called around or after the job call. # # def test_jobs # assert_performed_jobs 0 @@ -186,13 +190,14 @@ module ActiveJob # end # assert_performed_jobs 1 # - # perform_enqueued_jobs do - # HelloJob.perform_later('yves') - # assert_performed_jobs 2 - # end + # HelloJob.perform_later('yves') + # + # perform_enqueued_jobs + # + # assert_performed_jobs 2 # end # - # If a block is passed, that block should cause the specified number of + # If a block is passed, asserts that the block will cause the specified number of # jobs to be performed. # # def test_jobs_again @@ -206,7 +211,7 @@ module ActiveJob # end # end # - # The block form supports filtering. If the :only option is specified, + # This method also supports filtering. If the +:only+ option is specified, # then only the listed job(s) will be performed. # # def test_hello_job @@ -216,7 +221,7 @@ module ActiveJob # end # end # - # Also if the :except option is specified, + # Also if the +:except+ option is specified, # then the job(s) except specific class will be performed. # # def test_hello_job @@ -237,17 +242,30 @@ module ActiveJob # end # end # end - def assert_performed_jobs(number, only: nil, except: nil) + # + # If the +:queue+ option is specified, + # then only the job(s) enqueued to a specific queue will be performed. + # + # def test_assert_performed_jobs_with_queue_option + # assert_performed_jobs 1, queue: :some_queue do + # HelloJob.set(queue: :some_queue).perform_later("jeremy") + # HelloJob.set(queue: :other_queue).perform_later("bogdan") + # end + # end + def assert_performed_jobs(number, only: nil, except: nil, queue: nil, &block) if block_given? original_count = performed_jobs.size - perform_enqueued_jobs(only: only, except: except) { yield } + + perform_enqueued_jobs(only: only, except: except, queue: queue, &block) + new_count = performed_jobs.size - assert_equal number, new_count - original_count, - "#{number} jobs expected, but #{new_count - original_count} were performed" + + performed_jobs_size = new_count - original_count else - performed_jobs_size = performed_jobs.size - assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed" + performed_jobs_size = performed_jobs_with(only: only, except: except, queue: queue) end + + assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed" end # Asserts that no jobs have been performed. @@ -261,7 +279,7 @@ module ActiveJob # end # end # - # If a block is passed, that block should not cause any job to be performed. + # If a block is passed, asserts that the block will not cause any job to be performed. # # def test_jobs_again # assert_no_performed_jobs do @@ -269,7 +287,7 @@ module ActiveJob # end # end # - # The block form supports filtering. If the :only option is specified, + # The block form supports filtering. If the +:only+ option is specified, # then only the listed job(s) will not be performed. # # def test_no_logging @@ -278,7 +296,7 @@ module ActiveJob # end # end # - # Also if the :except option is specified, + # Also if the +:except+ option is specified, # then the job(s) except specific class will not be performed. # # def test_no_logging @@ -287,11 +305,20 @@ module ActiveJob # end # end # + # If the +:queue+ option is specified, + # then only the job(s) enqueued to a specific queue will not be performed. + # + # def test_assert_no_performed_jobs_with_queue_option + # assert_no_performed_jobs queue: :some_queue do + # HelloJob.set(queue: :other_queue).perform_later("jeremy") + # end + # end + # # Note: This assertion is simply a shortcut for: # # assert_performed_jobs 0, &block - def assert_no_performed_jobs(only: nil, except: nil, &block) - assert_performed_jobs 0, only: only, except: except, &block + def assert_no_performed_jobs(only: nil, except: nil, queue: nil, &block) + assert_performed_jobs 0, only: only, except: except, queue: queue, &block end # Asserts that the job has been enqueued with the given arguments. @@ -304,7 +331,23 @@ module ActiveJob # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) # end # - # If a block is passed, that block should cause the job to be + # + # The +args+ argument also accepts a proc which will get passed the actual + # job's arguments. Your proc needs to returns a boolean value determining if + # the job's arguments matches your expectation. This is useful to check only + # for a subset of arguments. + # + # def test_assert_enqueued_with + # expected_args = ->(job_args) do + # assert job_args.first.key?(:foo) + # end + # + # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test') + # assert_enqueued_with(job: MyJob, args: expected_args, queue: 'low') + # end + # + # + # If a block is passed, asserts that the block will cause the job to be # enqueued with the given arguments. # # def test_assert_enqueued_with @@ -318,7 +361,7 @@ module ActiveJob # end def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil) expected = { job: job, args: args, at: at, queue: queue }.compact - serialized_args = serialize_args_for_assertion(expected) + expected_args = prepare_args_for_assertion(expected) if block_given? original_enqueued_jobs_count = enqueued_jobs.count @@ -331,14 +374,56 @@ module ActiveJob end matching_job = jobs.find do |enqueued_job| - serialized_args.all? { |key, value| value == enqueued_job[key] } + deserialized_job = deserialize_args_for_assertion(enqueued_job) + + expected_args.all? do |key, value| + if value.respond_to?(:call) + value.call(deserialized_job[key]) + else + value == deserialized_job[key] + end + end end assert matching_job, "No enqueued job found with #{expected}" instantiate_job(matching_job) end - # Asserts that the job passed in the block has been performed with the given arguments. + # Asserts that the job has been performed with the given arguments. + # + # def test_assert_performed_with + # MyJob.perform_later(1,2,3) + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') + # + # MyJob.set(wait_until: Date.tomorrow.noon).perform_later + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) + # end + # + # The +args+ argument also accepts a proc which will get passed the actual + # job's arguments. Your proc needs to returns a boolean value determining if + # the job's arguments matches your expectation. This is useful to check only + # for a subset of arguments. + # + # def test_assert_performed_with + # expected_args = ->(job_args) do + # assert job_args.first.key?(:foo) + # end + # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test') + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, args: expected_args, queue: 'high') + # end + # + # If a block is passed, that block performs all of the jobs that were + # enqueued throughout the duration of the block and asserts that + # the job has been performed with the given arguments in the block. # # def test_assert_performed_with # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do @@ -349,20 +434,39 @@ module ActiveJob # MyJob.set(wait_until: Date.tomorrow.noon).perform_later # end # end - def assert_performed_with(job: nil, args: nil, at: nil, queue: nil) - original_performed_jobs_count = performed_jobs.count + def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block) expected = { job: job, args: args, at: at, queue: queue }.compact - serialized_args = serialize_args_for_assertion(expected) - perform_enqueued_jobs { yield } - in_block_jobs = performed_jobs.drop(original_performed_jobs_count) - matching_job = in_block_jobs.find do |in_block_job| - serialized_args.all? { |key, value| value == in_block_job[key] } + expected_args = prepare_args_for_assertion(expected) + + if block_given? + original_performed_jobs_count = performed_jobs.count + + perform_enqueued_jobs(&block) + + jobs = performed_jobs.drop(original_performed_jobs_count) + else + jobs = performed_jobs end + + matching_job = jobs.find do |enqueued_job| + deserialized_job = deserialize_args_for_assertion(enqueued_job) + + expected_args.all? do |key, value| + if value.respond_to?(:call) + value.call(deserialized_job[key]) + else + value == deserialized_job[key] + end + end + end + assert matching_job, "No performed job found with #{expected}" instantiate_job(matching_job) end - # Performs all enqueued jobs in the duration of the block. + # Performs all enqueued jobs. If a block is given, performs all of the jobs + # that were enqueued throughout the duration of the block. If a block is + # not given, performs all of the enqueued jobs up to this point in the test. # # def test_perform_enqueued_jobs # perform_enqueued_jobs do @@ -371,6 +475,14 @@ module ActiveJob # assert_performed_jobs 1 # end # + # def test_perform_enqueued_jobs_without_block + # MyJob.perform_later(1, 2, 3) + # + # perform_enqueued_jobs + # + # assert_performed_jobs 1 + # end + # # This method also supports filtering. If the +:only+ option is specified, # then only the listed job(s) will be performed. # @@ -393,24 +505,42 @@ module ActiveJob # assert_performed_jobs 1 # end # - def perform_enqueued_jobs(only: nil, except: nil) + # If the +:queue+ option is specified, + # then only the job(s) enqueued to a specific queue will be performed. + # + # def test_perform_enqueued_jobs_with_queue + # perform_enqueued_jobs queue: :some_queue do + # MyJob.set(queue: :some_queue).perform_later(1, 2, 3) # will be performed + # HelloJob.set(queue: :other_queue).perform_later(1, 2, 3) # will not be performed + # end + # assert_performed_jobs 1 + # end + # + def perform_enqueued_jobs(only: nil, except: nil, queue: nil) + return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given? + validate_option(only: only, except: except) + old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs old_filter = queue_adapter.filter old_reject = queue_adapter.reject + old_queue = queue_adapter.queue begin queue_adapter.perform_enqueued_jobs = true queue_adapter.perform_enqueued_at_jobs = true queue_adapter.filter = only queue_adapter.reject = except + queue_adapter.queue = queue + yield ensure queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs queue_adapter.filter = old_filter queue_adapter.reject = old_reject + queue_adapter.queue = old_queue end end @@ -432,31 +562,58 @@ module ActiveJob performed_jobs.clear end - def enqueued_jobs_size(only: nil, except: nil, queue: nil) + def jobs_with(jobs, only: nil, except: nil, queue: nil) validate_option(only: only, except: except) - enqueued_jobs.count do |job| + + jobs.count do |job| job_class = job.fetch(:job) + if only next false unless Array(only).include?(job_class) elsif except next false if Array(except).include?(job_class) end + if queue next false unless queue.to_s == job.fetch(:queue, job_class.queue_name) end + + yield job if block_given? + true end end - def serialize_args_for_assertion(args) - args.dup.tap do |serialized_args| - serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args] - serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at] + def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block) + jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block) + end + + def performed_jobs_with(only: nil, except: nil, queue: nil, &block) + jobs_with(performed_jobs, only: only, except: except, queue: queue, &block) + end + + def flush_enqueued_jobs(only: nil, except: nil, queue: nil) + enqueued_jobs_with(only: only, except: except, queue: queue) do |payload| + instantiate_job(payload).perform_now + queue_adapter.performed_jobs << payload + end + end + + def prepare_args_for_assertion(args) + args.dup.tap do |arguments| + arguments[:at] = arguments[:at].to_f if arguments[:at] + end + end + + def deserialize_args_for_assertion(job) + job.dup.tap do |new_job| + new_job[:args] = ActiveJob::Arguments.deserialize(new_job[:args]) if new_job[:args] end end def instantiate_job(payload) - job = payload[:job].new(*payload[:args]) + args = ActiveJob::Arguments.deserialize(payload[:args]) + job = payload[:job].new(*args) job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at) job.queue_name = payload[:queue] job diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index e5f1f087fe..8b2981926f 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -73,6 +73,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase string_key = { "a" => 1, "_aj_symbol_keys" => [] } another_string_key = { "a" => 1 } indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true } + indifferent_access_symbol_key = symbol_key.with_indifferent_access assert_equal( { a: 1 }, @@ -90,6 +91,10 @@ class ArgumentSerializationTest < ActiveSupport::TestCase { "a" => 1 }, ActiveJob::Arguments.deserialize([indifferent_access]).first ) + assert_equal( + { a: 1 }, + ActiveJob::Arguments.deserialize([indifferent_access_symbol_key]).first + ) end test "should maintain hash with indifferent access" do @@ -121,8 +126,10 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end test "should not allow reserved hash keys" do - ["_aj_globalid", :_aj_globalid, "_aj_symbol_keys", :_aj_symbol_keys, - "_aj_hash_with_indifferent_access", :_aj_hash_with_indifferent_access].each do |key| + ["_aj_globalid", :_aj_globalid, + "_aj_symbol_keys", :_aj_symbol_keys, + "_aj_hash_with_indifferent_access", :_aj_hash_with_indifferent_access, + "_aj_serialized", :_aj_serialized].each do |key| assert_raises ActiveJob::SerializationError do ActiveJob::Arguments.serialize [key => 1] end diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb index 47d4e3c0c2..37bb65538a 100644 --- a/activejob/test/cases/exceptions_test.rb +++ b/activejob/test/cases/exceptions_test.rb @@ -2,6 +2,7 @@ require "helper" require "jobs/retry_job" +require "models/person" class ExceptionsTest < ActiveJob::TestCase setup do @@ -131,4 +132,11 @@ class ExceptionsTest < ActiveJob::TestCase assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values end end + + test "successfully retry job throwing DeserializationError" do + perform_enqueued_jobs do + RetryJob.perform_later Person.new(404), 5 + assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values + end + end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index a1107a07fd..48ef39aaca 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -8,9 +8,11 @@ require "jobs/logging_job" require "jobs/overridden_logging_job" require "jobs/nested_job" require "jobs/rescue_job" +require "jobs/retry_job" require "models/person" class LoggingTest < ActiveSupport::TestCase + include ActiveJob::TestHelper include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity @@ -59,13 +61,17 @@ class LoggingTest < ActiveSupport::TestCase end def test_uses_job_name_as_tag - LoggingJob.perform_later "Dummy" - assert_match(/\[LoggingJob\]/, @logger.messages) + perform_enqueued_jobs do + LoggingJob.perform_later "Dummy" + assert_match(/\[LoggingJob\]/, @logger.messages) + end end def test_uses_job_id_as_tag - LoggingJob.perform_later "Dummy" - assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages) + perform_enqueued_jobs do + LoggingJob.perform_later "Dummy" + assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages) + end end def test_logs_correct_queue_name @@ -78,19 +84,23 @@ class LoggingTest < ActiveSupport::TestCase end def test_globalid_parameter_logging - person = Person.new(123) - LoggingJob.perform_later person - assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) - assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages) - assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + perform_enqueued_jobs do + person = Person.new(123) + LoggingJob.perform_later person + assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) + assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages) + assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + end end def test_globalid_nested_parameter_logging - person = Person.new(123) - LoggingJob.perform_later(person: person) - assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) - assert_match(%r{Dummy, here is it: .*#<Person:.*>}, @logger.messages) - assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + perform_enqueued_jobs do + person = Person.new(123) + LoggingJob.perform_later(person: person) + assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) + assert_match(%r{Dummy, here is it: .*#<Person:.*>}, @logger.messages) + assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + end end def test_enqueue_job_logging @@ -102,22 +112,26 @@ class LoggingTest < ActiveSupport::TestCase end def test_perform_job_logging - LoggingJob.perform_later "Dummy" - assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages) - assert_match(/Dummy, here is it: Dummy/, @logger.messages) - assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages) + perform_enqueued_jobs do + LoggingJob.perform_later "Dummy" + assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages) + assert_match(/Dummy, here is it: Dummy/, @logger.messages) + assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages) + end end def test_perform_nested_jobs_logging - NestedJob.perform_later - assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) - assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages) + perform_enqueued_jobs do + NestedJob.perform_later + assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) + assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages) + end end def test_enqueue_at_job_logging @@ -146,9 +160,45 @@ class LoggingTest < ActiveSupport::TestCase end def test_job_error_logging - RescueJob.perform_later "other" + perform_enqueued_jobs { RescueJob.perform_later "other" } rescue RescueJob::OtherError assert_match(/Performing RescueJob \(Job ID: .*?\) from .*? with arguments:.*other/, @logger.messages) assert_match(/Error performing RescueJob \(Job ID: .*?\) from .*? in .*ms: RescueJob::OtherError \(Bad hair\):\n.*\brescue_job\.rb:\d+:in `perform'/, @logger.messages) end + + def test_enqueue_retry_logging + perform_enqueued_jobs do + RetryJob.perform_later "DefaultsError", 2 + assert_match(/Retrying RetryJob in 3 seconds, due to a DefaultsError\./, @logger.messages) + end + end + + def test_enqueue_retry_logging_on_retry_job + perform_enqueued_jobs { RescueJob.perform_later "david" } + assert_match(/Retrying RescueJob in 0 seconds\./, @logger.messages) + end + + def test_retry_stopped_logging + perform_enqueued_jobs do + RetryJob.perform_later "CustomCatchError", 6 + assert_match(/Stopped retrying RetryJob due to a CustomCatchError, which reoccurred on \d+ attempts\./, @logger.messages) + end + end + + def test_retry_stopped_logging_without_block + perform_enqueued_jobs do + begin + RetryJob.perform_later "DefaultsError", 6 + rescue DefaultsError + assert_match(/Stopped retrying RetryJob due to a DefaultsError, which reoccurred on \d+ attempts\./, @logger.messages) + end + end + end + + def test_discard_logging + perform_enqueued_jobs do + RetryJob.perform_later "DiscardableError", 2 + assert_match(/Discarded RetryJob due to a DiscardableError\./, @logger.messages) + end + end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index d0a21a5da3..8dc32037ff 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -8,6 +8,7 @@ require "jobs/logging_job" require "jobs/nested_job" require "jobs/rescue_job" require "jobs/inherited_job" +require "jobs/multiple_kwargs_job" require "models/person" class EnqueuedJobsTest < ActiveJob::TestCase @@ -475,23 +476,23 @@ class EnqueuedJobsTest < ActiveJob::TestCase def test_assert_enqueued_with_returns job = assert_enqueued_with(job: LoggingJob) do - LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3) + LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3, keyword: true) end assert_instance_of LoggingJob, job assert_in_delta 5.minutes.from_now, job.scheduled_at, 1 assert_equal "default", job.queue_name - assert_equal [1, 2, 3], job.arguments + assert_equal [1, 2, 3, { keyword: true }], job.arguments end def test_assert_enqueued_with_with_no_block_returns - LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3) + LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3, keyword: true) job = assert_enqueued_with(job: LoggingJob) assert_instance_of LoggingJob, job assert_in_delta 5.minutes.from_now, job.scheduled_at, 1 assert_equal "default", job.queue_name - assert_equal [1, 2, 3], job.arguments + assert_equal [1, 2, 3, { keyword: true }], job.arguments end def test_assert_enqueued_with_failure @@ -503,7 +504,7 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_raise ActiveSupport::TestCase::Assertion do LoggingJob.perform_later - assert_enqueued_with(job: LoggingJob) {} + assert_enqueued_with(job: LoggingJob) { } end error = assert_raise ActiveSupport::TestCase::Assertion do @@ -537,6 +538,29 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_with_selective_args + args = ->(job_args) do + assert_equal 1, job_args.first[:argument1] + assert job_args.first[:argument2].key?(:b) + end + + assert_enqueued_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + + def test_assert_enqueued_with_selective_args_fails + args = ->(job_args) do + false + end + + assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + end + def test_assert_enqueued_with_with_no_block_args assert_raise ArgumentError do NestedJob.set(wait_until: Date.tomorrow.noon).perform_later @@ -555,6 +579,12 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon) end + def test_assert_enqueued_with_with_hash_arg + assert_enqueued_with(job: MultipleKwargsJob, args: [{ argument1: 1, argument2: { a: 1, b: 2 } }]) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + def test_assert_enqueued_with_with_global_id_args ricardo = Person.new(9) assert_enqueued_with(job: HelloJob, args: [ricardo]) do @@ -610,7 +640,7 @@ class EnqueuedJobsTest < ActiveJob::TestCase end class PerformedJobsTest < ActiveJob::TestCase - def test_performed_enqueue_jobs_with_only_option_doesnt_leak_outside_the_block + def test_perform_enqueued_jobs_with_only_option_doesnt_leak_outside_the_block assert_nil queue_adapter.filter perform_enqueued_jobs only: HelloJob do assert_equal HelloJob, queue_adapter.filter @@ -618,7 +648,13 @@ class PerformedJobsTest < ActiveJob::TestCase assert_nil queue_adapter.filter end - def test_performed_enqueue_jobs_with_except_option_doesnt_leak_outside_the_block + def test_perform_enqueued_jobs_without_block_with_only_option_doesnt_leak + perform_enqueued_jobs only: HelloJob + + assert_nil queue_adapter.filter + end + + def test_perform_enqueued_jobs_with_except_option_doesnt_leak_outside_the_block assert_nil queue_adapter.reject perform_enqueued_jobs except: HelloJob do assert_equal HelloJob, queue_adapter.reject @@ -626,6 +662,150 @@ class PerformedJobsTest < ActiveJob::TestCase assert_nil queue_adapter.reject end + def test_perform_enqueued_jobs_without_block_with_except_option_doesnt_leak + perform_enqueued_jobs except: HelloJob + + assert_nil queue_adapter.reject + end + + def test_perform_enqueued_jobs_with_queue_option_doesnt_leak_outside_the_block + assert_nil queue_adapter.queue + perform_enqueued_jobs queue: :some_queue do + assert_equal :some_queue, queue_adapter.queue + end + assert_nil queue_adapter.queue + end + + def test_perform_enqueued_jobs_without_block_with_queue_option_doesnt_leak + perform_enqueued_jobs queue: :some_queue + + assert_nil queue_adapter.reject + end + + def test_perform_enqueued_jobs_with_block + perform_enqueued_jobs do + HelloJob.perform_later("kevin") + LoggingJob.perform_later("bogdan") + end + + assert_performed_jobs 2 + end + + def test_perform_enqueued_jobs_without_block + HelloJob.perform_later("kevin") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + assert_performed_jobs 2 + end + + def test_perform_enqueued_jobs_with_block_with_only_option + perform_enqueued_jobs only: LoggingJob do + HelloJob.perform_later("kevin") + LoggingJob.perform_later("bogdan") + end + + assert_performed_jobs 1 + assert_performed_jobs 1, only: LoggingJob + end + + def test_perform_enqueued_jobs_without_block_with_only_option + HelloJob.perform_later("kevin") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs only: LoggingJob + + assert_performed_jobs 1 + assert_performed_jobs 1, only: LoggingJob + end + + def test_perform_enqueued_jobs_with_block_with_except_option + perform_enqueued_jobs except: HelloJob do + HelloJob.perform_later("kevin") + LoggingJob.perform_later("bogdan") + end + + assert_performed_jobs 1 + assert_performed_jobs 1, only: LoggingJob + end + + def test_perform_enqueued_jobs_without_block_with_except_option + HelloJob.perform_later("kevin") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs except: HelloJob + + assert_performed_jobs 1 + assert_performed_jobs 1, only: LoggingJob + end + + def test_perform_enqueued_jobs_with_block_with_queue_option + perform_enqueued_jobs queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later("kevin") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.perform_later("bogdan") + end + + assert_performed_jobs 1 + assert_performed_jobs 1, only: HelloJob, queue: :some_queue + end + + def test_perform_enqueued_jobs_without_block_with_queue_option + HelloJob.set(queue: :some_queue).perform_later("kevin") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs queue: :some_queue + + assert_performed_jobs 1 + assert_performed_jobs 1, only: HelloJob, queue: :some_queue + end + + def test_perform_enqueued_jobs_with_block_with_only_and_queue_options + perform_enqueued_jobs only: HelloJob, queue: :other_queue do + HelloJob.set(queue: :some_queue).perform_later("kevin") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("bogdan") + end + + assert_performed_jobs 1 + assert_performed_jobs 1, only: HelloJob, queue: :other_queue + end + + def test_perform_enqueued_jobs_without_block_with_only_and_queue_options + HelloJob.set(queue: :some_queue).perform_later("kevin") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("bogdan") + + perform_enqueued_jobs only: HelloJob, queue: :other_queue + + assert_performed_jobs 1 + assert_performed_jobs 1, only: HelloJob, queue: :other_queue + end + + def test_perform_enqueued_jobs_with_block_with_except_and_queue_options + perform_enqueued_jobs except: HelloJob, queue: :other_queue do + HelloJob.set(queue: :other_queue).perform_later("kevin") + LoggingJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("bogdan") + end + + assert_performed_jobs 1 + assert_performed_jobs 1, only: LoggingJob, queue: :other_queue + end + + def test_perform_enqueued_jobs_without_block_with_except_and_queue_options + HelloJob.set(queue: :other_queue).perform_later("kevin") + LoggingJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("bogdan") + + perform_enqueued_jobs except: HelloJob, queue: :other_queue + + assert_performed_jobs 1 + assert_performed_jobs 1, only: LoggingJob, queue: :other_queue + end + def test_assert_performed_jobs assert_nothing_raised do assert_performed_jobs 1 do @@ -731,6 +911,28 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_jobs_without_block_with_only_option + HelloJob.perform_later("jeremy") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + assert_performed_jobs 1, only: HelloJob + end + + def test_assert_performed_jobs_without_block_with_only_option_failure + LoggingJob.perform_later("jeremy") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, only: HelloJob + end + + assert_match(/1 .* but 0/, error.message) + end + def test_assert_performed_jobs_with_except_option assert_nothing_raised do assert_performed_jobs 1, except: LoggingJob do @@ -740,6 +942,28 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_jobs_without_block_with_except_option + HelloJob.perform_later("jeremy") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + assert_performed_jobs 1, except: HelloJob + end + + def test_assert_performed_jobs_without_block_with_except_option_failure + HelloJob.perform_later("jeremy") + HelloJob.perform_later("bogdan") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, except: HelloJob + end + + assert_match(/1 .* but 0/, error.message) + end + def test_assert_performed_jobs_with_only_and_except_option error = assert_raise ArgumentError do assert_performed_jobs 1, only: HelloJob, except: HelloJob do @@ -751,6 +975,19 @@ class PerformedJobsTest < ActiveJob::TestCase assert_match(/`:only` and `:except`/, error.message) end + def test_assert_performed_jobs_without_block_with_only_and_except_options + error = assert_raise ArgumentError do + HelloJob.perform_later("jeremy") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + assert_performed_jobs 1, only: HelloJob, except: HelloJob + end + + assert_match(/`:only` and `:except`/, error.message) + end + def test_assert_performed_jobs_with_only_option_as_array assert_nothing_raised do assert_performed_jobs 2, only: [HelloJob, LoggingJob] do @@ -876,6 +1113,134 @@ class PerformedJobsTest < ActiveJob::TestCase assert_match(/`:only` and `:except`/, error.message) end + def test_assert_performed_jobs_with_queue_option + assert_performed_jobs 1, queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + end + end + + def test_assert_performed_jobs_with_queue_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + end + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_performed_jobs_without_block_with_queue_option + HelloJob.set(queue: :some_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + + perform_enqueued_jobs + + assert_performed_jobs 1, queue: :some_queue + end + + def test_assert_performed_jobs_without_block_with_queue_option_failure + HelloJob.set(queue: :other_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, queue: :some_queue + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_performed_jobs_with_only_and_queue_options + assert_performed_jobs 1, only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + def test_assert_performed_jobs_with_only_and_queue_options_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_performed_jobs_without_block_with_only_and_queue_options + HelloJob.set(queue: :some_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + assert_performed_jobs 1, only: HelloJob, queue: :some_queue + end + + def test_assert_performed_jobs_without_block_with_only_and_queue_options_failure + HelloJob.set(queue: :other_queue).perform_later("jeremy") + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, only: HelloJob, queue: :some_queue + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_performed_jobs_with_except_and_queue_options + assert_performed_jobs 1, except: HelloJob, queue: :other_queue do + HelloJob.set(queue: :other_queue).perform_later("jeremy") + LoggingJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("jeremy") + end + end + + def test_assert_performed_jobs_with_except_and_queue_options_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, except: HelloJob, queue: :other_queue do + HelloJob.set(queue: :other_queue).perform_later("jeremy") + LoggingJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_performed_jobs_without_block_with_except_and_queue_options + HelloJob.set(queue: :other_queue).perform_later("jeremy") + LoggingJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("jeremy") + + perform_enqueued_jobs + + assert_performed_jobs 1, except: HelloJob, queue: :other_queue + end + + def test_assert_performed_jobs_without_block_with_except_and_queue_options_failure + HelloJob.set(queue: :other_queue).perform_later("jeremy") + LoggingJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, except: HelloJob, queue: :other_queue + end + + assert_match(/1 .* but 0/, error.message) + end + def test_assert_no_performed_jobs_with_only_option assert_nothing_raised do assert_no_performed_jobs only: HelloJob do @@ -884,6 +1249,26 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_no_performed_jobs_without_block_with_only_option + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + assert_no_performed_jobs only: HelloJob + end + + def test_assert_no_performed_jobs_without_block_with_only_option_failure + HelloJob.perform_later("bogdan") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs only: HelloJob + end + + assert_match(/0 .* but 1/, error.message) + end + def test_assert_no_performed_jobs_with_except_option assert_nothing_raised do assert_no_performed_jobs except: LoggingJob do @@ -892,6 +1277,26 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_no_performed_jobs_without_block_with_except_option + HelloJob.perform_later("jeremy") + + perform_enqueued_jobs + + assert_no_performed_jobs except: HelloJob + end + + def test_assert_no_performed_jobs_without_block_with_except_option_failure + LoggingJob.perform_later("jeremy") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs except: HelloJob + end + + assert_match(/0 .* but 1/, error.message) + end + def test_assert_no_performed_jobs_with_only_and_except_option error = assert_raise ArgumentError do assert_no_performed_jobs only: HelloJob, except: HelloJob do @@ -902,6 +1307,19 @@ class PerformedJobsTest < ActiveJob::TestCase assert_match(/`:only` and `:except`/, error.message) end + def test_assert_no_performed_jobs_without_block_with_only_and_except_options + error = assert_raise ArgumentError do + HelloJob.perform_later("jeremy") + LoggingJob.perform_later("bogdan") + + perform_enqueued_jobs + + assert_no_performed_jobs only: HelloJob, except: HelloJob + end + + assert_match(/`:only` and `:except`/, error.message) + end + def test_assert_no_performed_jobs_with_only_option_as_array assert_nothing_raised do assert_no_performed_jobs only: [HelloJob, RescueJob] do @@ -962,24 +1380,165 @@ class PerformedJobsTest < ActiveJob::TestCase assert_match(/`:only` and `:except`/, error.message) end - def test_assert_performed_job + def test_assert_no_performed_jobs_with_queue_option + assert_no_performed_jobs queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later("jeremy") + end + end + + def test_assert_no_performed_jobs_with_queue_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_performed_jobs_without_block_with_queue_option + HelloJob.set(queue: :other_queue).perform_later("jeremy") + + perform_enqueued_jobs + + assert_no_performed_jobs queue: :some_queue + end + + def test_assert_no_performed_jobs_without_block_with_queue_option_failure + HelloJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs queue: :some_queue + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_performed_jobs_with_only_and_queue_options + assert_no_performed_jobs only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + def test_assert_no_performed_jobs_with_only_and_queue_options_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_performed_jobs_without_block_with_only_and_queue_options + HelloJob.set(queue: :other_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + assert_no_performed_jobs only: HelloJob, queue: :some_queue + end + + def test_assert_no_performed_jobs_without_block_with_only_and_queue_options_failure + HelloJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs only: HelloJob, queue: :some_queue + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_performed_jobs_with_except_and_queue_options + assert_no_performed_jobs except: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later("bogdan") + HelloJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("jeremy") + end + end + + def test_assert_no_performed_jobs_with_except_and_queue_options_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs except: HelloJob, queue: :some_queue do + HelloJob.set(queue: :other_queue).perform_later("bogdan") + HelloJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + end + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_no_performed_jobs_without_block_with_except_and_queue_options + HelloJob.set(queue: :other_queue).perform_later("bogdan") + HelloJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :other_queue).perform_later("jeremy") + + perform_enqueued_jobs + + assert_no_performed_jobs except: HelloJob, queue: :some_queue + end + + def test_assert_no_performed_jobs_without_block_with_except_and_queue_options_failure + HelloJob.set(queue: :other_queue).perform_later("bogdan") + HelloJob.set(queue: :some_queue).perform_later("bogdan") + LoggingJob.set(queue: :some_queue).perform_later("jeremy") + + perform_enqueued_jobs + + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs except: HelloJob, queue: :some_queue + end + + assert_match(/0 .* but 1/, error.message) + end + + def test_assert_performed_with assert_performed_with(job: NestedJob, queue: "default") do NestedJob.perform_later end end - def test_assert_performed_job_returns - job = assert_performed_with(job: NestedJob, queue: "default") do - NestedJob.perform_later + def test_assert_performed_with_without_block + NestedJob.perform_later + + perform_enqueued_jobs + + assert_performed_with(job: NestedJob, queue: "default") + end + + def test_assert_performed_with_returns + job = assert_performed_with(job: LoggingJob, queue: "default") do + LoggingJob.perform_later(keyword: :sym) end - assert_instance_of NestedJob, job + assert_instance_of LoggingJob, job assert_nil job.scheduled_at - assert_equal [], job.arguments + assert_equal [{ keyword: :sym }], job.arguments assert_equal "default", job.queue_name end - def test_assert_performed_job_failure + def test_assert_performed_with_without_block_returns + LoggingJob.perform_later(keyword: :sym) + + perform_enqueued_jobs + + job = assert_performed_with(job: LoggingJob, queue: "default") + + assert_instance_of LoggingJob, job + assert_nil job.scheduled_at + assert_equal [{ keyword: :sym }], job.arguments + assert_equal "default", job.queue_name + end + + def test_assert_performed_with_failure assert_raise ActiveSupport::TestCase::Assertion do assert_performed_with(job: LoggingJob) do HelloJob.perform_later @@ -993,7 +1552,23 @@ class PerformedJobsTest < ActiveJob::TestCase end end - def test_assert_performed_job_with_at_option + def test_assert_performed_with_without_block_failure + HelloJob.perform_later + + perform_enqueued_jobs + + assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: LoggingJob) + end + + HelloJob.set(queue: "important").perform_later + + assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: HelloJob, queue: "low") + end + end + + def test_assert_performed_with_with_at_option assert_performed_with(job: HelloJob, at: Date.tomorrow.noon) do HelloJob.set(wait_until: Date.tomorrow.noon).perform_later end @@ -1005,14 +1580,66 @@ class PerformedJobsTest < ActiveJob::TestCase end end - def test_assert_performed_job_with_global_id_args + def test_assert_performed_with_without_block_with_at_option + HelloJob.set(wait_until: Date.tomorrow.noon).perform_later + + perform_enqueued_jobs + + assert_performed_with(job: HelloJob, at: Date.tomorrow.noon) + + HelloJob.set(wait_until: Date.tomorrow.noon).perform_later + + perform_enqueued_jobs + + assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: HelloJob, at: Date.today.noon) + end + end + + def test_assert_performed_with_with_hash_arg + assert_performed_with(job: MultipleKwargsJob, args: [{ argument1: 1, argument2: { a: 1, b: 2 } }]) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + + def test_assert_performed_with_selective_args + args = ->(job_args) do + assert_equal 1, job_args.first[:argument1] + assert job_args.first[:argument2].key?(:b) + end + + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + + def test_assert_performed_with_selective_args_fails + args = ->(job_args) do + false + end + + assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + end + + def test_assert_performed_with_with_global_id_args ricardo = Person.new(9) assert_performed_with(job: HelloJob, args: [ricardo]) do HelloJob.perform_later(ricardo) end end - def test_assert_performed_job_failure_with_global_id_args + def test_assert_performed_with_without_bllock_with_global_id_args + ricardo = Person.new(9) + HelloJob.perform_later(ricardo) + perform_enqueued_jobs + assert_performed_with(job: HelloJob, args: [ricardo]) + end + + def test_assert_performed_with_failure_with_global_id_args ricardo = Person.new(9) wilma = Person.new(11) error = assert_raise ActiveSupport::TestCase::Assertion do @@ -1024,7 +1651,19 @@ class PerformedJobsTest < ActiveJob::TestCase assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message end - def test_assert_performed_job_does_not_change_jobs_count + def test_assert_performed_with_without_block_failure_with_global_id_args + ricardo = Person.new(9) + wilma = Person.new(11) + HelloJob.perform_later(ricardo) + perform_enqueued_jobs + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: HelloJob, args: [wilma]) + end + + assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message + end + + def test_assert_performed_with_does_not_change_jobs_count assert_performed_with(job: HelloJob) do HelloJob.perform_later end @@ -1035,6 +1674,18 @@ class PerformedJobsTest < ActiveJob::TestCase assert_equal 2, queue_adapter.performed_jobs.count end + + def test_assert_performed_with_without_block_does_not_change_jobs_count + HelloJob.perform_later + perform_enqueued_jobs + assert_performed_with(job: HelloJob) + + perform_enqueued_jobs + HelloJob.perform_later + assert_performed_with(job: HelloJob) + + assert_equal 2, queue_adapter.performed_jobs.count + end end class OverrideQueueAdapterTest < ActiveJob::TestCase diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 32afb5ca62..96253773c7 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -137,4 +137,16 @@ class QueuingTest < ActiveSupport::TestCase assert job_executed "#{@id}.2" assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1") end + + test "should run job with higher priority first in Backburner" do + skip unless adapter_is?(:backburner) + + jobs_manager.tube.pause(3) + TestJob.set(priority: 20).perform_later "#{@id}.1" + TestJob.set(priority: 10).perform_later "#{@id}.2" + wait_for_jobs_to_finish_for(10.seconds) + assert job_executed "#{@id}.1" + assert job_executed "#{@id}.2" + assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1") + end end diff --git a/activejob/test/jobs/multiple_kwargs_job.rb b/activejob/test/jobs/multiple_kwargs_job.rb new file mode 100644 index 0000000000..b355c4ce1a --- /dev/null +++ b/activejob/test/jobs/multiple_kwargs_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "../support/job_buffer" + +class MultipleKwargsJob < ActiveJob::Base + def perform(argument1:, argument2:) + JobBuffer.add("Job with argument1: #{argument1}, argument2: #{argument2}") + end +end diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb index 1383fffd7d..68dc17e16c 100644 --- a/activejob/test/jobs/retry_job.rb +++ b/activejob/test/jobs/retry_job.rb @@ -24,6 +24,7 @@ class RetryJob < ActiveJob::Base retry_on ExponentialWaitTenAttemptsError, wait: :exponentially_longer, attempts: 10 retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10 retry_on(CustomCatchError) { |job, error| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{error.message}") } + retry_on(ActiveJob::DeserializationError) { |job, error| JobBuffer.add("Raised #{error.class} for the #{job.executions} time") } discard_on DiscardableError discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb index eb179011d9..1163ae8178 100644 --- a/activejob/test/support/integration/adapters/backburner.rb +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -8,7 +8,8 @@ module BackburnerJobsManager end unless can_run? puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n" - exit + status = ENV["CI"] ? false : true + exit status end end diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index 2a771b08c7..f231e5e12d 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -18,8 +18,8 @@ module QueJobsManager user = uri.user || ENV["USER"] pass = uri.password db = uri.path[1..-1] - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'drop database if exists "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'create database "#{db}"' -U #{user} -t template1} Que.connection = Sequel.connect(que_url) Que.migrate! @@ -32,7 +32,8 @@ module QueJobsManager rescue Sequel::DatabaseConnectionError puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" - exit + status = ENV["CI"] ? false : true + exit status end def stop_workers diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index 1b0685a971..2b5375461a 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -17,8 +17,8 @@ module QueueClassicJobsManager user = uri.user || ENV["USER"] pass = uri.password db = uri.path[1..-1] - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'drop database if exists "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'create database "#{db}"' -U #{user} -t template1} QC::Setup.create QC.default_conn_adapter.disconnect @@ -30,7 +30,8 @@ module QueueClassicJobsManager rescue PG::ConnectionBad puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" - exit + status = ENV["CI"] ? false : true + exit status end def stop_workers diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb index 965e6e2e6c..eb8d4cc2d5 100644 --- a/activejob/test/support/integration/adapters/sneakers.rb +++ b/activejob/test/support/integration/adapters/sneakers.rb @@ -1,19 +1,8 @@ # frozen_string_literal: true require "sneakers/runner" -require "sneakers/publisher" require "timeout" -module Sneakers - class Publisher - def safe_ensure_connected - @mutex.synchronize do - ensure_connection! unless connected? - end - end - end -end - module SneakersJobsManager def setup ActiveJob::Base.queue_adapter = :sneakers @@ -29,7 +18,8 @@ module SneakersJobsManager log: Rails.root.join("log/sneakers.log").to_s unless can_run? puts "Cannot run integration tests for sneakers. To be able to run integration tests for sneakers you need to install and start rabbitmq.\n" - exit + status = ENV["CI"] ? false : true + exit status end end @@ -79,7 +69,7 @@ module SneakersJobsManager def bunny_publisher @bunny_publisher ||= begin p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher) - p.safe_ensure_connected + p.ensure_connection! p end end diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb index a02d874e2e..c5fa2b136f 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -puts "\n\n*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" +puts "\n\n*** rake test:integration:#{ENV['AJ_ADAPTER']} ***\n" ENV["RAILS_ENV"] = "test" ActiveJob::Base.queue_name_prefix = nil diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 8f80838a33..6048911217 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,8 +1,31 @@ +* Fix numericality validator to still use value before type cast except Active Record. + + Fixes #33651, #33686. + + *Ryuta Kamizono* + +* Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps. + + Before: + ``` + contact = Contact.new(created_at: Time.utc(2006, 8, 1)) + contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC + ``` + + After: + ``` + contact = Contact.new(created_at: Time.utc(2006, 8, 1)) + contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z" + ``` + + *Bogdan Gusiev* + * Allows configurable attribute name for `#has_secure_password`. This still defaults to an attribute named 'password', causing no breaking change. There is a new method `#authenticate_XXX` where XXX is the configured attribute name, making the existing `#authenticate` now an alias for this when the attribute is the default 'password'. + Example: class User < ActiveRecord::Base @@ -14,7 +37,7 @@ user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..." user.authenticate_recovery_password('42password') # => user - *Unathi Chonco* + *Unathi Chonco* * Add `config.active_model.i18n_full_message` in order to control whether the `full_message` error format can be overridden at the attribute or model diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 7be466dc4c..493fca698a 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -25,5 +25,8 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version end diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index 3f19cda07b..75f60d205e 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -206,6 +206,7 @@ module ActiveModel raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" end alias_method :with_value_from_user, :with_value_from_database + alias_method :with_cast_value, :with_value_from_database end class Uninitialized < Attribute # :nodoc: diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 217bf1ac01..f0e3458f51 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -27,7 +27,7 @@ module ActiveModel # cat.status # => 'sleeping' def assign_attributes(new_attributes) if !new_attributes.respond_to?(:stringify_keys) - raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." + raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed." end return if new_attributes.empty? diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 888a431e5f..d8352343e9 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -369,7 +369,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) + extra = (extra.map!(&:inspect) << "*args").join(", ") target = if CALL_COMPILABLE_REGEXP.match?(send) "#{"self." unless include_private}#{send}(#{extra})" @@ -474,5 +474,43 @@ module ActiveModel def _read_attribute(attr) __send__(attr) end + + module AttrNames # :nodoc: + DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/ + + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes in read_attribute. + def self.define_attribute_accessor_method(mod, attr_name, writer: false) + method_name = "#{attr_name}#{'=' if writer}" + if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name) + yield method_name, "'#{attr_name}'.freeze" + else + safe_name = attr_name.unpack1("h*") + const_name = "ATTR_#{safe_name}" + const_set(const_name, attr_name) unless const_defined?(const_name) + temp_method_name = "__temp__#{safe_name}#{'=' if writer}" + attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}" + yield temp_method_name, attr_name_expr + mod.send(:alias_method, method_name, temp_method_name) + mod.send(:undef_method, temp_method_name) + end + end + end end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index 5bf213d593..c3a446098c 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -29,17 +29,16 @@ module ActiveModel private def define_method_attribute=(name) - safe_name = name.unpack1("h*".freeze) - ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name - - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name, writer: true, + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name}(value) + name = #{attr_name_expr} + write_attribute(name, value) + end + RUBY + end end NO_DEFAULT_PROVIDED = Object.new # :nodoc: @@ -97,15 +96,4 @@ module ActiveModel write_attribute(attribute_name, value) end end - - module AttributeMethods #:nodoc: - AttrNames = Module.new { - def self.set_name_cache(name, value) - const_name = "ATTR_#{name}" - unless const_defined? const_name - const_set const_name, -value - end - end - } - end end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index cdc1282817..82713ddc81 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -103,7 +103,7 @@ module ActiveModel @_to_partial_path ||= begin element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name)) collection = ActiveSupport::Inflector.tableize(name) - "#{collection}/#{element}".freeze + "#{collection}/#{element}" end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 093052a70c..f0e1242554 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -151,7 +151,7 @@ module ActiveModel @mutations_from_database = nil end - # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise. + # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise. # # person.changed? # => false # person.name = 'bob' diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index edc30ee64d..af94d52d45 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -379,9 +379,11 @@ module ActiveModel # * <tt>errors.format</tt> def full_message(attribute, message) return message if attribute == :base + attribute = attribute.to_s if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope) - parts = attribute.to_s.split(".") + attribute = attribute.remove(/\[\d\]/) + parts = attribute.split(".") attribute_name = parts.pop namespace = parts.join("/") unless parts.empty? attributes_scope = "#{@base.class.i18n_scope}.errors.models" @@ -410,8 +412,9 @@ module ActiveModel defaults << :"errors.format" defaults << "%{attribute} %{message}" - attr_name = attribute.to_s.tr(".", "_").humanize + attr_name = attribute.tr(".", "_").humanize attr_name = @base.class.human_attribute_name(attribute, default: attr_name) + I18n.t(defaults.shift, default: defaults, attribute: attr_name, diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 983401801f..bf23fa3c05 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -209,7 +209,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr("/".freeze, "_".freeze) + ActiveSupport::Inflector.underscore(string).tr("/", "_") end end @@ -252,7 +252,7 @@ module ActiveModel # Person.model_name.plural # => "people" def model_name @_model_name ||= begin - namespace = parents.detect do |n| + namespace = module_parents.detect do |n| n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming? end ActiveModel::Name.new(self, namespace) diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 25e1541d66..f77fb98c32 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -26,13 +26,13 @@ module ActiveModel # user = User.find(1) # user.as_json # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true} + # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true} # # ActiveRecord::Base.include_root_in_json = true # # user.as_json # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true } } + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } } # # This behavior can also be achieved by setting the <tt>:root</tt> option # to +true+ as in: @@ -40,7 +40,7 @@ module ActiveModel # user = User.find(1) # user.as_json(root: true) # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true } } + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } } # # Without any +options+, the returned Hash will include all the model's # attributes. @@ -48,7 +48,7 @@ module ActiveModel # user = User.find(1) # user.as_json # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true} + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true} # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit # the attributes included, and work similar to the +attributes+ method. @@ -63,14 +63,14 @@ module ActiveModel # # user.as_json(methods: :permalink) # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true, + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # # "permalink" => "1-konata-izumi" } # # To include associations use <tt>:include</tt>: # # user.as_json(include: :posts) # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true, + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" }, # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] } # @@ -81,7 +81,7 @@ module ActiveModel # only: :body } }, # only: :title } }) # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true, + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ], # # "title" => "Welcome to the weblog" }, # # { "comments" => [ { "body" => "Don't think too hard" } ], @@ -93,11 +93,12 @@ module ActiveModel include_root_in_json end + hash = serializable_hash(options).as_json if root root = model_name.element if root == true - { root => serializable_hash(options) } + { root => hash } else - serializable_hash(options) + hash end end diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb index 9641bf45ee..d48598376e 100644 --- a/activemodel/lib/active_model/type/date_time.rb +++ b/activemodel/lib/active_model/type/date_time.rb @@ -39,9 +39,9 @@ module ActiveModel end def value_from_multiparameter_assignment(values_hash) - missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } - if missing_parameter - raise ArgumentError, missing_parameter + missing_parameters = (1..3).select { |key| !values_hash.key?(key) } + if missing_parameters.any? + raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}" end super end diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index 16e14f9e5f..473cdb0c67 100644 --- a/activemodel/lib/active_model/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -29,7 +29,7 @@ module ActiveModel # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - value.to_s !~ /\A-?\d+\.?\d*\z/ + !/\A[-+]?\d+/.match?(value.to_s) end end end diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index cb6aa67a9d..da56073436 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -70,7 +70,13 @@ module ActiveModel # Doesn't handle time zones. def fast_string_to_time(string) if string =~ ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i + microsec_part = $7 + if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7 + microsec_part[0] = "" + microsec = microsec_part.to_i + else + microsec = (microsec_part.to_r * 1_000_000).to_i + end new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb index 36f13945b1..a9c9bfadb6 100644 --- a/activemodel/lib/active_model/type/string.rb +++ b/activemodel/lib/active_model/type/string.rb @@ -16,8 +16,8 @@ module ActiveModel def cast_value(value) case value when ::String then ::String.new(value) - when true then "t".freeze - when false then "f".freeze + when true then "t" + when false then "f" else value.to_s end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 0478915be7..126a87ac6e 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -21,8 +21,17 @@ module ActiveModel def validate_each(record, attr_name, value) came_from_user = :"#{attr_name}_came_from_user?" - if record.respond_to?(came_from_user) && record.public_send(came_from_user) - raw_value = record.read_attribute_before_type_cast(attr_name) + if record.respond_to?(came_from_user) + if record.public_send(came_from_user) + raw_value = record.read_attribute_before_type_cast(attr_name) + elsif record.respond_to?(:read_attribute) + raw_value = record.read_attribute(attr_name) + end + else + before_type_cast = :"#{attr_name}_before_type_cast" + if record.respond_to?(before_type_cast) + raw_value = record.public_send(before_type_cast) + end end raw_value ||= value diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 88cca318ef..21c4ce0dfe 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -116,7 +116,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?("::".freeze) ? key.constantize : const_get(key) + validator = key.include?("::") ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index b06291f1b4..30e8419685 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -100,9 +100,11 @@ class AttributeAssignmentTest < ActiveModel::TestCase end test "an ArgumentError is raised if a non-hash-like object is passed" do - assert_raises(ArgumentError) do + err = assert_raises(ArgumentError) do Model.new(1) end + + assert_equal("When assigning attributes, you must pass a hash as an argument, Integer passed.", err.message) end test "forbidden attributes cannot be used for mass assignment" do diff --git a/activemodel/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb index b868dba743..62feb9074e 100644 --- a/activemodel/test/cases/attribute_set_test.rb +++ b/activemodel/test/cases/attribute_set_test.rb @@ -217,7 +217,7 @@ module ActiveModel assert_equal({ foo: "1" }, attributes.to_hash) end - test "marshaling dump/load legacy materialized attribute hash" do + test "marshalling dump/load legacy materialized attribute hash" do builder = AttributeSet::Builder.new(foo: Type::String.new) attributes = builder.build_from_database(foo: "1") diff --git a/activemodel/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb index ea2b0efd11..20c02e689c 100644 --- a/activemodel/test/cases/attribute_test.rb +++ b/activemodel/test/cases/attribute_test.rb @@ -78,7 +78,7 @@ module ActiveModel end test "duping dups the value" do - @type.expect(:deserialize, "type cast".dup, ["a value"]) + @type.expect(:deserialize, +"type cast", ["a value"]) attribute = Attribute.from_database(nil, "a value", @type) value_from_orig = attribute.value @@ -246,7 +246,7 @@ module ActiveModel end test "with_type preserves mutations" do - attribute = Attribute.from_database(:foo, "".dup, Type::Value.new) + attribute = Attribute.from_database(:foo, +"", Type::Value.new) attribute.value << "1" assert_equal 1, attribute.with_type(Type::Integer.new).value diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 1ec12d8222..0711dc56ca 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -112,7 +112,7 @@ class CallbacksTest < ActiveModel::TestCase def callback1; history << "callback1"; end def callback2; history << "callback2"; end def create - run_callbacks(:create) {} + run_callbacks(:create) { } self end end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index b38d84fff2..0edbbffa86 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -94,7 +94,7 @@ class DirtyTest < ActiveModel::TestCase end test "attribute mutation" do - @model.instance_variable_set("@name", "Yam".dup) + @model.instance_variable_set("@name", +"Yam") assert_not_predicate @model, :name_changed? @model.name.replace("Hadad") assert_not_predicate @model, :name_changed? diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 91fb9d0a7c..138b1d1bb9 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -14,12 +14,14 @@ require "active_support/testing/method_call_assertions" class ActiveModel::TestCase < ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index aae98c9fe4..625e0a427a 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -129,6 +129,10 @@ class JsonSerializationTest < ActiveModel::TestCase assert_equal :name, options[:except] end + test "as_json should serialize timestamps" do + assert_equal "2006-08-01T00:00:00.000Z", @contact.as_json["created_at"] + end + test "as_json should return a hash if include_root_in_json is true" do begin original_include_root_in_json = Contact.include_root_in_json @@ -138,7 +142,7 @@ class JsonSerializationTest < ActiveModel::TestCase assert_kind_of Hash, json assert_kind_of Hash, json["contact"] %w(name age created_at awesome preferences).each do |field| - assert_equal @contact.send(field), json["contact"][field] + assert_equal @contact.send(field).as_json, json["contact"][field] end ensure Contact.include_root_in_json = original_include_root_in_json diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb index 60f62becc2..74b47d1b4d 100644 --- a/activemodel/test/cases/type/date_time_test.rb +++ b/activemodel/test/cases/type/date_time_test.rb @@ -25,6 +25,17 @@ module ActiveModel end end + def test_hash_to_time + type = Type::DateTime.new + assert_equal ::Time.utc(2018, 10, 15, 0, 0, 0), type.cast(1 => 2018, 2 => 10, 3 => 15) + end + + def test_hash_with_wrong_keys + type = Type::DateTime.new + error = assert_raises(ArgumentError) { type.cast(a: 1) } + assert_equal "Provided hash {:a=>1} doesn't contain necessary keys: [1, 2, 3]", error.message + end + private def with_timezone_config(default:) diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index c0cf6ce590..be60c4f7fa 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -57,9 +57,12 @@ module ActiveModel def test_changed? type = Decimal.new - assert type.changed?(5.0, 5.0, "5.0wibble") + assert type.changed?(0.0, 0, "wibble") + assert type.changed?(5.0, 0, "wibble") + assert_not type.changed?(5.0, 5.0, "5.0wibble") assert_not type.changed?(5.0, 5.0, "5.0") assert_not type.changed?(-5.0, -5.0, "-5.0") + assert_not type.changed?(5.0, 5.0, "0.5e+1") end def test_scale_is_applied_before_precision_to_prevent_rounding_errors diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb index 28318e06f8..230a8dda32 100644 --- a/activemodel/test/cases/type/float_test.rb +++ b/activemodel/test/cases/type/float_test.rb @@ -21,9 +21,12 @@ module ActiveModel def test_changing_float type = Type::Float.new - assert type.changed?(5.0, 5.0, "5wibble") + assert type.changed?(0.0, 0, "wibble") + assert type.changed?(5.0, 0, "wibble") + assert_not type.changed?(5.0, 5.0, "5wibble") assert_not type.changed?(5.0, 5.0, "5") assert_not type.changed?(5.0, 5.0, "5.0") + assert_not type.changed?(500.0, 500.0, "0.5E+4") assert_not type.changed?(nil, nil, nil) end end diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb index 8c5d18c9b3..df12098974 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -53,9 +53,13 @@ module ActiveModel test "changed?" do type = Type::Integer.new - assert type.changed?(5, 5, "5wibble") + assert type.changed?(0, 0, "wibble") + assert type.changed?(5, 0, "wibble") + assert_not type.changed?(5, 5, "5wibble") assert_not type.changed?(5, 5, "5") assert_not type.changed?(5, 5, "5.0") + assert_not type.changed?(5, 5, "+5") + assert_not type.changed?(5, 5, "+5.0") assert_not type.changed?(-5, -5, "-5") assert_not type.changed?(-5, -5, "-5.0") assert_not type.changed?(nil, nil, nil) diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb index 825c8bb246..2d85556d20 100644 --- a/activemodel/test/cases/type/string_test.rb +++ b/activemodel/test/cases/type/string_test.rb @@ -15,11 +15,11 @@ module ActiveModel test "cast strings are mutable" do type = Type::String.new - s = "foo".dup + s = +"foo" assert_equal false, type.cast(s).frozen? assert_equal false, s.frozen? - f = "foo".freeze + f = -"foo" assert_equal false, type.cast(f).frozen? assert_equal true, f.frozen? end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 1c62e750de..ccb565c5bd 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -35,7 +35,7 @@ class I18nValidationTest < ActiveModel::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, "not found") - assert_called_with(Person, :human_attribute_name, [:name, default: "Name"], returns: "Person's name") do + assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do assert_equal ["Person's name not found"], @person.errors.full_messages end end @@ -101,6 +101,63 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank") end + def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) + + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } }) + + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + attributes: { 'person/contacts/addresses': { country: "Country" } } + }) + + person = Person.new + assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config + ActiveModel::Errors.i18n_full_message = false + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) + + person = Person.new + assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config + ActiveModel::Errors.i18n_full_message = false + + I18n.backend.store_translations("en", activemodel: { + attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } } + }) + + person = Person.new + assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + # ActiveModel::Validations # A set of common cases for ActiveModel::Validations message generation that diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index 01b78ae72e..ca3c3bc40d 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -262,6 +262,16 @@ class NumericalityValidationTest < ActiveModel::TestCase Person.clear_validators! end + def test_validates_numericality_using_value_before_type_cast_if_possible + Topic.validates_numericality_of :price + + topic = Topic.new(price: 50) + + assert_equal "$50.00", topic.price + assert_equal 50, topic.price_before_type_cast + assert_predicate topic, :valid? + end + def test_validates_numericality_with_exponent_number base = 10_000_000_000_000_000 Topic.validates_numericality_of :approved, less_than_or_equal_to: base diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index b0af00ee45..db3284f833 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -3,6 +3,11 @@ class Topic include ActiveModel::Validations include ActiveModel::Validations::Callbacks + include ActiveModel::AttributeMethods + include ActiveSupport::NumberHelper + + attribute_method_suffix "_before_type_cast" + define_attribute_method :price def self._validates_default_keys super | [ :message ] @@ -10,6 +15,7 @@ class Topic attr_accessor :title, :author_name, :content, :approved, :created_at attr_accessor :after_validation_performed + attr_writer :price after_validation :perform_after_validation @@ -38,4 +44,12 @@ class Topic def my_validation_with_arg(attr) errors.add attr, "is missing" unless send(attr) end + + def price + number_to_currency @price + end + + def attribute_before_type_cast(attr) + instance_variable_get(:"@#{attr}") + end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d1ae41ab97..2a237f86cf 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,297 @@ +* Add an `:if_not_exists` option to `create_table`. + + Example: + + create_table :posts, if_not_exists: true do |t| + t.string :title + end + + That would execute: + + CREATE TABLE IF NOT EXISTS posts ( + ... + ) + + If the table already exists, `if_not_exists: false` (the default) raises an + exception whereas `if_not_exists: true` does nothing. + + *fatkodima*, *Stefan Kanev* + +* Defining an Enum as a Hash with blank key, or as an Array with a blank value, now raises an `ArgumentError`. + + *Christophe Maximin* + +* Adds support for multiple databases to `rails db:schema:cache:dump` and `rails db:schema:cache:clear`. + + *Gannon McGibbon* + +* `update_columns` now correctly raises `ActiveModel::MissingAttributeError` + if the attribute does not exist. + + *Sean Griffin* + +* Add support for hash and url configs in database hash of `ActiveRecord::Base.connected_to`. + + ```` + User.connected_to(database: { writing: "postgres://foo" }) do + User.create!(name: "Gannon") + end + + config = { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + User.connected_to(database: { reading: config }) do + User.count + end + ```` + + *Gannon McGibbon* + +* Support default expression for MySQL. + + MySQL 8.0.13 and higher supports default value to be a function or expression. + + https://dev.mysql.com/doc/refman/8.0/en/create-table.html + + *Ryuta Kamizono* + +* Support expression indexes for MySQL. + + MySQL 8.0.13 and higher supports functional key parts that index + expression values rather than column or column prefix values. + + https://dev.mysql.com/doc/refman/8.0/en/create-index.html + + *Ryuta Kamizono* + +* Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error. + + Fixes #33056. + + *Federico Martinez* + +* Add basic API for connection switching to support multiple databases. + + 1) Adds a `connects_to` method for models to connect to multiple databases. Example: + + ``` + class AnimalsModel < ApplicationRecord + self.abstract_class = true + + connects_to database: { writing: :animals_primary, reading: :animals_replica } + end + + class Dog < AnimalsModel + # connected to both the animals_primary db for writing and the animals_replica for reading + end + ``` + + 2) Adds a `connected_to` block method for switching connection roles or connecting to + a database that the model didn't connect to. Connecting to the database in this block is + useful when you have another defined connection, for example `slow_replica` that you don't + want to connect to by default but need in the console, or a specific code block. + + ``` + ActiveRecord::Base.connected_to(role: :reading) do + Dog.first # finds dog from replica connected to AnimalsBase + Book.first # doesn't have a reading connection, will raise an error + end + ``` + + ``` + ActiveRecord::Base.connected_to(database: :slow_replica) do + SlowReplicaModel.first # if the db config has a slow_replica configuration this will be used to do the lookup, otherwise this will throw an exception + end + ``` + + *Eileen M. Uchitelle* + +* Enum raises on invalid definition values + + When defining a Hash enum it can be easy to use [] instead of {}. This + commit checks that only valid definition values are provided, those can + be a Hash, an array of Symbols or an array of Strings. Otherwise it + raises an ArgumentError. + + Fixes #33961 + + *Alberto Almagro* + +* Reloading associations now clears the Query Cache like `Persistence#reload` does. + + ``` + class Post < ActiveRecord::Base + has_one :category + belongs_to :author + has_many :comments + end + + # Each of the following will now clear the query cache. + post.reload_category + post.reload_author + post.comments.reload + ``` + + *Christophe Maximin* + +* Added `index` option for `change_table` migration helpers. + With this change you can create indexes while adding new + columns into the existing tables. + + Example: + + change_table(:languages) do |t| + t.string :country_code, index: true + end + + *Mehmet Emin İNAÇ* + +* Fix `transaction` reverting for migrations. + + Before: Commands inside a `transaction` in a reverted migration ran uninverted. + Now: This change fixes that by reverting commands inside `transaction` block. + + *fatkodima*, *David Verhasselt* + +* Raise an error instead of scanning the filesystem root when `fixture_path` is blank. + + *Gannon McGibbon*, *Max Albrecht* + +* Allow `ActiveRecord::Base.configurations=` to be set with a symbolized hash. + + *Gannon McGibbon* + +* Don't update counter cache unless the record is actually saved. + + Fixes #31493, #33113, #33117. + + *Ryuta Kamizono* + +* Deprecate `ActiveRecord::Result#to_hash` in favor of `ActiveRecord::Result#to_a`. + + *Gannon McGibbon*, *Kevin Cheng* + +* SQLite3 adapter supports expression indexes. + + ``` + create_table :users do |t| + t.string :email + end + + add_index :users, 'lower(email)', name: 'index_users_on_email', unique: true + ``` + + *Gray Kemmey* + +* Allow subclasses to redefine autosave callbacks for associated records. + + Fixes #33305. + + *Andrey Subbota* + +* Bump minimum MySQL version to 5.5.8. + + *Yasuo Honda* + +* Use MySQL utf8mb4 character set by default. + + `utf8mb4` character set with 4-Byte encoding supports supplementary characters including emoji. + The previous default 3-Byte encoding character set `utf8` is not enough to support them. + + *Yasuo Honda* + +* Fix duplicated record creation when using nested attributes with `create_with`. + + *Darwin Wu* + +* Configuration item `config.filter_parameters` could also filter out + sensitive values of database columns when call `#inspect`. + We also added `ActiveRecord::Base::filter_attributes`/`=` in order to + specify sensitive attributes to specific model. + + ``` + Rails.application.config.filter_parameters += [:credit_card_number, /phone/] + Account.last.inspect # => #<Account id: 123, name: "DHH", credit_card_number: [FILTERED], telephone_number: [FILTERED] ...> + SecureAccount.filter_attributes += [:name] + SecureAccount.last.inspect # => #<SecureAccount id: 42, name: [FILTERED], credit_card_number: [FILTERED] ...> + ``` + + *Zhang Kang*, *Yoshiyuki Kinjo* + +* Deprecate `column_name_length`, `table_name_length`, `columns_per_table`, + `indexes_per_table`, `columns_per_multicolumn_index`, `sql_query_length`, + and `joins_per_query` methods in `DatabaseLimits`. + + *Ryuta Kamizono* + +* `ActiveRecord::Base.configurations` now returns an object. + + `ActiveRecord::Base.configurations` used to return a hash, but this + is an inflexible data model. In order to improve multiple-database + handling in Rails, we've changed this to return an object. Some methods + are provided to make the object behave hash-like in order to ease the + transition process. Since most applications don't manipulate the hash + we've decided to add backwards-compatible functionality that will throw + a deprecation warning if used, however calling `ActiveRecord::Base.configurations` + will use the new version internally and externally. + + For example, the following `database.yml`: + + ``` + development: + adapter: sqlite3 + database: db/development.sqlite3 + ``` + + Used to become a hash: + + ``` + { "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } } + ``` + + Is now converted into the following object: + + ``` + #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}> + ] + ``` + + Iterating over the database configurations has also changed. Instead of + calling hash methods on the `configurations` hash directly, a new method `configs_for` has + been provided that allows you to select the correct configuration. `env_name`, and + `spec_name` arguments are optional. For example these return an array of + database config objects for the requested environment and a single database config object + will be returned for the requested environment and specification name respectively. + + ``` + ActiveRecord::Base.configurations.configs_for(env_name: "development") + ActiveRecord::Base.configurations.configs_for(env_name: "development", spec_name: "primary") + ``` + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Add database configuration to disable advisory locks. + + ``` + production: + adapter: postgresql + advisory_locks: false + ``` + + *Guo Xiang* + +* SQLite3 adapter `alter_table` method restores foreign keys. + + *Yasuo Honda* + +* Allow `:to_table` option to `invert_remove_foreign_key`. + + Example: + + remove_foreign_key :accounts, to_table: :owners + + *Nikolay Epifanov*, *Rich Chen* + * Add environment & load_config dependency to `bin/rake db:seed` to enable seed load in environments without Rails and custom DB configuration @@ -64,8 +358,8 @@ *Eddie Lebow* -* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in - ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database. +* Add `ActiveRecord::Base.create_or_find_by`/`!` to deal with the SELECT/INSERT race condition in + `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database. *DHH* diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 170c95b827..fae56a51bb 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -94,8 +94,8 @@ namespace :db do desc "Build the MySQL test databases" task :build do config = ARTest.config["connections"]["mysql2"] - %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) + %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) end desc "Drop the MySQL test databases" diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index a857d00c05..bcdd82052c 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -28,6 +28,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activerecord/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "activemodel", version end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 1a2c78f39b..024e503ec7 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -176,7 +176,7 @@ Benchmark.ips(TIME) do |x| end x.report "Model.log" do - Exhibit.connection.send(:log, "hello", "world") {} + Exhibit.connection.send(:log, "hello", "world") { } end x.report "AR.execute(query)" do diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d198466dbf..d43378c64f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,7 +40,6 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache - autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 403667fb70..4c538ef2bd 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -31,9 +31,9 @@ module ActiveRecord private def exec_queries - super do |r| - @association.set_inverse_instance r - yield r if block_given? + super do |record| + @association.set_inverse_instance_from_queries(record) + yield record if block_given? end end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1ee52945ea..fb1df00dc8 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1294,7 +1294,7 @@ module ActiveRecord # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records. + # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records. # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. # # If using with the <tt>:through</tt> option, the association on the join model must be @@ -1437,7 +1437,7 @@ module ActiveRecord # * <tt>:destroy</tt> causes the associated object to also be destroyed # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. @@ -1524,6 +1524,7 @@ module ActiveRecord # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, and sets it as the foreign key. + # No modification or deletion of existing records takes place. # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. @@ -1581,7 +1582,7 @@ module ActiveRecord # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. - # By default this is id. + # By default this is +id+. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. @@ -1761,6 +1762,7 @@ module ActiveRecord # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } # has_and_belongs_to_many :categories, ->(post) { # where("default_category = ?", post.default_category) + # } # # === Extensions # diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 44596f4424..bf4942aac8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -40,7 +40,9 @@ module ActiveRecord end # Reloads the \target and returns +self+ on success. - def reload + # The QueryCache is cleared if +force+ is true. + def reload(force = false) + klass.connection.clear_query_cache if force && klass reset reset_scope load_target @@ -79,18 +81,6 @@ module ActiveRecord target_scope.merge!(association_scope) end - # The scope for this association. - # - # Note that the association_scope is merged into the target_scope only when the - # scope method is called. This is because at that point the call may be surrounded - # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which - # actually gets built. - def association_scope - if klass - @association_scope ||= AssociationScope.scope(self) - end - end - def reset_scope @association_scope = nil end @@ -103,6 +93,13 @@ module ActiveRecord record end + def set_inverse_instance_from_queries(record) + if inverse = inverse_association_for(record) + inverse.inversed_from_queries(owner) + end + record + end + # Remove the inverse association, if possible def remove_inverse_instance(record) if inverse = inverse_association_for(record) @@ -114,6 +111,7 @@ module ActiveRecord self.target = record @inversed = !!record end + alias :inversed_from_queries :inversed_from # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. @@ -121,12 +119,6 @@ module ActiveRecord reflection.klass end - # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the - # through association's scope) - def target_scope - AssociationRelation.create(klass, self).merge!(klass.all) - end - def extensions extensions = klass.default_extensions | reflection.extensions @@ -187,6 +179,24 @@ module ActiveRecord end private + # The scope for this association. + # + # Note that the association_scope is merged into the target_scope only when the + # scope method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or unscoped { ... } etc, which affects the scope which + # actually gets built. + def association_scope + if klass + @association_scope ||= AssociationScope.scope(self) + end + end + + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + AssociationRelation.create(klass, self).merge!(klass.all) + end + def scope_for_create scope.scope_for_create end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 3d4ad1dd5b..3346725f2d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -34,14 +34,28 @@ module ActiveRecord @updated end - def decrement_counters # :nodoc: + def decrement_counters update_counters(-1) end - def increment_counters # :nodoc: + def increment_counters update_counters(1) end + def decrement_counters_before_last_save + if reflection.polymorphic? + model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize) + else + model_was = klass + end + + foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key) + + if foreign_key_was && model_was < ActiveRecord::Base + update_counters_via_scope(model_was, foreign_key_was, -1) + end + end + def target_changed? owner.saved_change_to_attribute?(reflection.foreign_key) end @@ -50,11 +64,8 @@ module ActiveRecord def replace(record) if record raise_on_type_mismatch!(record) - update_counters_on_replace(record) set_inverse_instance(record) @updated = true - else - decrement_counters end replace_keys(record) @@ -67,11 +78,16 @@ module ActiveRecord if target && !stale_target? target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) + update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by) end end end + def update_counters_via_scope(klass, foreign_key, by) + scope = klass.unscoped.where!(primary_key(klass) => foreign_key) + scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) + end + def find_target? !loaded? && foreign_key_present? && klass end @@ -80,25 +96,12 @@ module ActiveRecord reflection.counter_cache_column && owner.persisted? end - def update_counters_on_replace(record) - if require_counter_update? && different_target?(record) - owner.instance_variable_set :@_after_replace_counter_called, true - record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch]) - decrement_counters - end - end - - # Checks whether record is different to the current target, without loading it - def different_target?(record) - record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key) - end - def replace_keys(record) - owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil + owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record.class)) : nil end - def primary_key(record) - reflection.association_primary_key(record.class) + def primary_key(klass) + reflection.association_primary_key(klass) end def foreign_key_present? @@ -112,11 +115,6 @@ module ActiveRecord inverse && inverse.has_one? end - def counter_cache_target - primary_key = reflection.association_primary_key(klass) - klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key)) - end - def stale_state result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) } result && result.to_s diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 3fd2fb5f67..9ae452e7a1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -19,10 +19,6 @@ module ActiveRecord owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil end - def different_target?(record) - super || record.class != klass - end - def inverse_reflection_for(record) reflection.polymorphic_inverse_of(record.class) end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index b5fb0092d7..fc00f1e900 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -21,56 +21,16 @@ module ActiveRecord::Associations::Builder # :nodoc: add_default_callbacks(model, reflection) if reflection.options[:default] end - def self.define_accessors(mixin, reflection) - super - add_counter_cache_methods mixin - end - - def self.add_counter_cache_methods(mixin) - return if mixin.method_defined? :belongs_to_counter_cache_after_update - - mixin.class_eval do - def belongs_to_counter_cache_after_update(reflection) - foreign_key = reflection.foreign_key - cache_column = reflection.counter_cache_column - - if (@_after_replace_counter_called ||= false) - @_after_replace_counter_called = false - elsif association(reflection.name).target_changed? - if reflection.polymorphic? - model = attribute_in_database(reflection.foreign_type).try(:constantize) - model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) - else - model = reflection.klass - model_was = reflection.klass - end - - foreign_key_was = attribute_before_last_save foreign_key - foreign_key = attribute_in_database foreign_key - - if foreign_key && model < ActiveRecord::Base - counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1) - end - - if foreign_key_was && model_was < ActiveRecord::Base - counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1) - end - end - end - - private - def counter_cache_target(reflection, model, foreign_key) - primary_key = reflection.association_primary_key(model) - model.unscoped.where!(primary_key => foreign_key) - end - end - end - def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column model.after_update lambda { |record| - record.belongs_to_counter_cache_after_update(reflection) + association = association(reflection.name) + + if association.target_changed? + association.increment_counters + association.decrement_counters_before_last_save + end } klass = reflection.class_name.safe_constantize @@ -121,12 +81,18 @@ module ActiveRecord::Associations::Builder # :nodoc: BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) }} - unless reflection.counter_cache_column + if reflection.counter_cache_column + touch_callback = callback.(:saved_changes) + update_callback = lambda { |record| + instance_exec(record, &touch_callback) unless association(reflection.name).target_changed? + } + model.after_update update_callback, if: :saved_changes? + else model.after_create callback.(:saved_changes), if: :saved_changes? + model.after_update callback.(:saved_changes), if: :saved_changes? model.after_destroy callback.(:changes_to_save) end - model.after_update callback.(:saved_changes), if: :saved_changes? model.after_touch callback.(:changes_to_save) end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 35a72c3850..ff57c40121 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -24,7 +24,7 @@ module ActiveRecord::Associations::Builder # :nodoc: if block_given? extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" extension = Module.new(&Proc.new) - model.parent.const_set(extension_module_name, extension) + model.module_parent.const_set(extension_module_name, extension) end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index e3070e0472..0140aa15c8 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -63,7 +63,7 @@ module ActiveRecord::Associations::Builder # :nodoc: def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym + association_name].join("_").gsub("::", "_").to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 9a30198b95..811fbfd927 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1088,7 +1088,7 @@ module ActiveRecord # person.pets.reload # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload - proxy_association.reload + proxy_association.reload(true) reset_scope end @@ -1125,7 +1125,7 @@ module ActiveRecord SpawnMethods, ].flat_map { |klass| klass.public_instance_methods(false) - } - self.public_instance_methods(false) - [:select] + [:scoping] + } - self.public_instance_methods(false) - [:select] + [:scoping, :values] delegate(*delegate_methods, to: :scope) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 617956c768..f84ac65fa2 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -21,20 +21,6 @@ module ActiveRecord super end - def concat_records(records) - ensure_not_nested - - records = super(records, true) - - if owner.new_record? && records - records.flatten.each do |record| - build_through_record(record) - end - end - - records - end - def insert_record(record, validate = true, raise = false) ensure_not_nested @@ -48,6 +34,20 @@ module ActiveRecord end private + def concat_records(records) + ensure_not_nested + + records = super(records, true) + + if owner.new_record? && records + records.flatten.each do |record| + build_through_record(record) + end + end + + records + end + # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 5c2ac5b374..8997579527 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -88,7 +88,6 @@ module ActiveRecord if records.empty? [] else - records.uniq! Array.wrap(associations).flat_map { |association| preloaders_on association, records, preload_scope } @@ -102,10 +101,8 @@ module ActiveRecord case association when Hash preloaders_for_hash(association, records, scope, polymorphic_parent) - when Symbol + when Symbol, String preloaders_for_one(association, records, scope, polymorphic_parent) - when String - preloaders_for_one(association.to_sym, records, scope, polymorphic_parent) else raise ArgumentError, "#{association.inspect} was not recognized for preload" end @@ -113,23 +110,21 @@ module ActiveRecord def preloaders_for_hash(association, records, scope, polymorphic_parent) association.flat_map { |parent, child| - loaders = preloaders_for_one parent, records, scope, polymorphic_parent - - recs = loaders.flat_map(&:preloaded_records).uniq - - reflection = records.first.class._reflect_on_association(parent) - polymorphic_parent = reflection && reflection.options[:polymorphic] - - loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope, polymorphic_parent - } - loaders + grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records| + loaders = preloaders_for_reflection(reflection, reflection_records, scope) + recs = loaders.flat_map(&:preloaded_records) + child_polymorphic_parent = reflection && reflection.options[:polymorphic] + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope, child_polymorphic_parent + } + loaders + end } end # Loads all the given data into +records+ for a singular +association+. # - # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # Functions by instantiating a preloader class such as Preloader::Association and # call the +run+ method for each passed in class in the +records+ argument. # # Not all records have the same class, so group then preload group on the reflection @@ -140,12 +135,17 @@ module ActiveRecord # classes, depending on the polymorphic_type field. So we group by the classes as # well. def preloaders_for_one(association, records, scope, polymorphic_parent) - grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses| - klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) - loader.run self - loader + grouped_records(association, records, polymorphic_parent) + .flat_map do |reflection, reflection_records| + preloaders_for_reflection reflection, reflection_records, scope end + end + + def preloaders_for_reflection(reflection, records, scope) + records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs| + loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) + loader.run self + loader end end @@ -153,11 +153,9 @@ module ActiveRecord h = {} records.each do |record| next unless record - next if polymorphic_parent && !record.class._reflect_on_association(association) - assoc = record.association(association) - next unless assoc.klass - klasses = h[assoc.reflection] ||= {} - (klasses[assoc.klass] ||= []) << record + reflection = record.class._reflect_on_association(association) + next if polymorphic_parent && !reflection || !record.association(association).klass + (h[reflection] ||= []) << record end h end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index cfab16a745..8e50cce102 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -26,7 +26,7 @@ module ActiveRecord # Implements the reload reader method, e.g. foo.reload_bar for # Foo.has_one :bar def force_reload_reader - klass.uncached { reload } + reload(true) target end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e4b8b1a330..fd8c1da842 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -22,16 +22,7 @@ module ActiveRecord delegate :column_for_attribute, to: :class end - AttrNames = Module.new { - def self.set_name_cache(name, value) - const_name = "ATTR_#{name}" - unless const_defined? const_name - const_set const_name, -value - end - end - } - - BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) + RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module #:nodoc: include Mutex_m @@ -123,7 +114,7 @@ module ActiveRecord # A class method is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) def dangerous_class_method?(method_name) - BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) + RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: @@ -167,12 +158,14 @@ module ActiveRecord end end - # Regexp whitelist. Matches the following: + # Regexp for column names (with or without a table name prefix). Matches + # the following: # "#{table_name}.#{column_name}" # "#{column_name}" - COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i - # Regexp whitelist. Matches the following: + # Regexp for column names with order (with or without a table name + # prefix, with or without various order modifiers). Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" # "#{table_name}.#{column_name} #{direction} NULLS FIRST" @@ -181,7 +174,7 @@ module ActiveRecord # "#{column_name} #{direction}" # "#{column_name} #{direction} NULLS FIRST" # "#{column_name} NULLS LAST" - COLUMN_NAME_ORDER_WHITELIST = / + COLUMN_NAME_WITH_ORDER = / \A (?:\w+\.)? \w+ @@ -190,12 +183,10 @@ module ActiveRecord \z /ix - def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: + def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc: unexpected = args.reject do |arg| - arg.kind_of?(Arel::Node) || - arg.is_a?(Arel::Nodes::SqlLiteral) || - arg.is_a?(Arel::Attributes::Attribute) || - arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) } + Arel.arel_node?(arg) || + arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) } end return if unexpected.none? @@ -270,21 +261,14 @@ module ActiveRecord def respond_to?(name, include_private = false) return false unless super - case name - when :to_partial_path - name = "to_partial_path".freeze - when :to_model - name = "to_model".freeze - else - name = name.to_s - end - # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && self.class.column_names.include?(name) - return has_attribute?(name) + if defined?(@attributes) + if name = self.class.symbol_column_to_string(name.to_sym) + return has_attribute?(name) + end end true @@ -344,15 +328,8 @@ module ActiveRecord # person.attribute_for_inspect(:tag_ids) # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) - value = read_attribute(attr_name) - - if value.is_a?(String) && value.length > 50 - "#{value[0, 50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end + value = _read_attribute(attr_name) + format_for_inspect(value) end # Returns +true+ if the specified +attribute+ has been set by the user or by a @@ -449,14 +426,6 @@ module ActiveRecord defined?(@attributes) && @attributes.key?(attr_name) end - def attributes_with_values_for_create(attribute_names) - attributes_with_values(attributes_for_create(attribute_names)) - end - - def attributes_with_values_for_update(attribute_names) - attributes_with_values(attributes_for_update(attribute_names)) - end - def attributes_with_values(attribute_names) attribute_names.each_with_object({}) do |name, attrs| attrs[name] = _read_attribute(name) @@ -465,7 +434,8 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| readonly_attribute?(name) end end @@ -473,11 +443,22 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| pk_attribute?(name) && id.nil? end end + def format_for_inspect(value) + if value.is_a?(String) && value.length > 50 + "#{value[0, 50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + end + def readonly_attribute?(name) self.class.readonly_attributes.include?(name) end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 233ee29fac..ebc2252c50 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -16,9 +16,6 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false, default: true - after_create { changes_applied } - after_update { changes_applied } - # Attribute methods for "changed in last call to save?" attribute_method_affix(prefix: "saved_change_to_", suffix: "?") attribute_method_prefix("saved_change_to_") @@ -167,16 +164,20 @@ module ActiveRecord result end - def _update_record(*) - partial_writes? ? super(keys_for_partial_write) : super + def _update_record(attribute_names = attribute_names_for_partial_writes) + affected_rows = super + changes_applied + affected_rows end - def _create_record(*) - partial_writes? ? super(keys_for_partial_write) : super + def _create_record(attribute_names = attribute_names_for_partial_writes) + id = super + changes_applied + id end - def keys_for_partial_write - changed_attribute_names_to_save & self.class.column_names + def attribute_names_for_partial_writes + partial_writes? ? changed_attribute_names_to_save : attribute_names end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0f7bcba564..6e1275e990 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -8,42 +8,19 @@ module ActiveRecord module ClassMethods # :nodoc: private - # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch. - # Evaluating many similar methods may use more memory as the instruction - # sequences are duplicated and cached (in MRI). define_method may - # be slower on dispatch, but if you're careful about the closure - # created, then define_method will consume much less memory. - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes in read_attribute. def define_method_attribute(name) - safe_name = name.unpack1("h*".freeze) - temp_method = "__temp__#{safe_name}" - - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - #{sync_with_transaction_state} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR - - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name} + #{sync_with_transaction_state} + name = #{attr_name_expr} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + RUBY end end end @@ -52,14 +29,13 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key - name = primary_key if name == "id".freeze && primary_key + name = primary_key if name == "id" && primary_key sync_with_transaction_state if name == primary_key _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index d2b7817b45..294a3dc32c 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -73,7 +73,7 @@ module ActiveRecord # `skip_time_zone_conversion_for_attributes` would not be picked up. subclass.class_eval do matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } - decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type| TimeZoneConverter.new(type) end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c7521422bb..455e67e19b 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,19 +13,19 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack1("h*".freeze) - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - #{sync_with_transaction_state} - _write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name, writer: true, + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name}(value) + name = #{attr_name_expr} + #{sync_with_transaction_state} + _write_attribute(name, value) + end + RUBY + end end end @@ -33,14 +33,13 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key - name = primary_key if name == "id".freeze && primary_key + name = primary_key if name == "id" && primary_key sync_with_transaction_state if name == primary_key _write_attribute(name, value) end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a405f05e0b..d77d76cb1e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -149,7 +149,7 @@ module ActiveRecord private def define_non_cyclic_method(name, &block) - return if method_defined?(name) + return if instance_methods(false).include?(name) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations @@ -392,7 +392,7 @@ module ActiveRecord records -= records_to_destroy end - records.each_with_index do |record, index| + records.each do |record| next if record.destroyed? saved = true @@ -401,11 +401,11 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) elsif !reflection.nested? + association_saved = association.insert_record(record) + if reflection.validate? - valid = association_valid?(reflection, record, index) - saved = valid ? association.insert_record(record, false) : false - else - association.insert_record(record) + errors.add(reflection.name) unless association_saved + saved = association_saved end end elsif autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5169f312f5..db097cb930 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require "active_record/explain_subscriber" require "active_record/relation/delegation" require "active_record/attributes" require "active_record/type_caster" +require "active_record/database_configurations" module ActiveRecord #:nodoc: # = Active Record @@ -291,7 +292,6 @@ module ActiveRecord #:nodoc: extend Aggregations::ClassMethods include Core - include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index b6852bfc71..5407af85ea 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -95,7 +95,7 @@ module ActiveRecord # # private # def delete_parents - # self.class.delete_all "parent_id = #{id}" + # self.class.where(parent_id: id).delete_all # end # end # @@ -318,7 +318,7 @@ module ActiveRecord _run_touch_callbacks { super } end - def increment!(*, touch: nil) # :nodoc: + def increment!(attribute, by = 1, touch: nil) # :nodoc: touch ? _run_touch_callbacks { super } : super end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index dfba78614e..4b6db8a96c 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -16,13 +16,13 @@ module ActiveRecord collection = collection.send(:apply_join_dependency) end column_type = type_for_attribute(timestamp_column) - column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column)) + column = connection.visitor.compile(collection.arel_attribute(timestamp_column)) select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" if collection.has_limit_or_offset? - query = collection.select(column) + query = collection.select("#{column} AS collection_cache_key_timestamp") subquery_alias = "subquery_for_cache_key" - subquery_column = "#{subquery_alias}.#{timestamp_column}" + subquery_column = "#{subquery_alias}.collection_cache_key_timestamp" subquery = query.arel.as(subquery_alias) arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) else diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index f721e91203..99934a0e31 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -188,7 +188,9 @@ module ActiveRecord t0 = Time.now elapsed = 0 loop do - @cond.wait(timeout - elapsed) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @cond.wait(timeout - elapsed) + end return remove if any? @@ -729,7 +731,7 @@ module ActiveRecord # this block can't be easily moved into attempt_to_checkout_all_existing_connections's # rescue block, because doing so would put it outside of synchronize section, without # being in a critical section thread_report might become inaccurate - msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds".dup + msg = +"could not obtain ownership of all database connections in #{checkout_timeout} seconds" thread_report = [] @connections.each do |conn| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 7a9e7add24..1305216be2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits @@ -12,11 +14,13 @@ module ActiveRecord def column_name_length 64 end + deprecate :column_name_length # Returns the maximum length of a table name. def table_name_length 64 end + deprecate :table_name_length # Returns the maximum allowed length for an index name. This # limit is enforced by \Rails and is less than or equal to @@ -36,16 +40,19 @@ module ActiveRecord def columns_per_table 1024 end + deprecate :columns_per_table # Returns the maximum number of indexes per table. def indexes_per_table 16 end + deprecate :indexes_per_table # Returns the maximum number of columns in a multicolumn index. def columns_per_multicolumn_index 16 end + deprecate :columns_per_multicolumn_index # Returns the maximum number of elements in an IN (x,y,z) clause. # +nil+ means no limit. @@ -57,11 +64,18 @@ module ActiveRecord def sql_query_length 1048575 end + deprecate :sql_query_length # Returns maximum number of joins in a single query. def joins_per_query 256 end + deprecate :joins_per_query + + private + def bind_params_length + 65535 + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 41553cfa83..0059f0b773 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -20,7 +20,7 @@ module ActiveRecord raise "Passing bind parameters with an arel AST is forbidden. " \ "The values must be stored on the AST directly" end - sql, binds = visitor.accept(arel_or_sql_string.ast, collector).value + sql, binds = visitor.compile(arel_or_sql_string.ast, collector) [sql.freeze, binds || []] else [arel_or_sql_string.dup.freeze, binds] @@ -32,11 +32,11 @@ module ActiveRecord # can be used to query the database repeatedly. def cacheable_query(klass, arel) # :nodoc: if prepared_statements - sql, binds = visitor.accept(arel.ast, collector).value + sql, binds = visitor.compile(arel.ast, collector) query = klass.query(sql) else - collector = PartialQueryCollector.new - parts, binds = visitor.accept(arel.ast, collector).value + collector = klass.partial_query_collector + parts, binds = visitor.compile(arel.ast, collector) query = klass.partial_query(parts) end [query, binds] @@ -46,11 +46,16 @@ module ActiveRecord def select_all(arel, name = nil, binds = [], preparable: nil) arel = arel_from_relation(arel) sql, binds = to_sql_and_binds(arel, binds) + if !prepared_statements || (arel.is_a?(String) && preparable.nil?) preparable = false + elsif binds.length > bind_params_length + sql, binds = unprepared_statement { to_sql_and_binds(arel) } + preparable = false else preparable = visitor.preparable end + if prepared_statements && preparable select_prepared(sql, name, binds) else @@ -259,7 +264,9 @@ module ActiveRecord attr_reader :transaction_manager #:nodoc: - delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, + :commit_transaction, :rollback_transaction, :materialize_transactions, + :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager def transaction_open? current_transaction.open? @@ -372,7 +379,7 @@ module ActiveRecord build_fixture_sql(fixtures, table_name) end.compact - table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" } total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) disable_referential_integrity do @@ -403,16 +410,6 @@ module ActiveRecord end end - # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work - # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in - # an UPDATE statement, so in the MySQL adapters we redefine this to do that. - def join_to_update(update, select, key) # :nodoc: - subselect = subquery_for(key, select) - - update.where key.in(subselect) - end - alias join_to_delete join_to_update - private def default_insert_value(column) Arel.sql("DEFAULT") @@ -453,13 +450,6 @@ module ActiveRecord total_sql.join(";\n") end - # Returns a subquery for the given key using the join information. - def subquery_for(key, select) - subselect = select.clone - subselect.projections = [key] - subselect - end - # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) exec_query(sql, name, binds, prepare: false) @@ -500,28 +490,6 @@ module ActiveRecord value end end - - class PartialQueryCollector - def initialize - @parts = [] - @binds = [] - end - - def <<(str) - @parts << str - self - end - - def add_bind(obj) - @binds << obj - @parts << Arel::Nodes::BindParam.new(1) - self - end - - def value - [@parts, @binds] - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 98b1348135..07e86afe9a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -60,7 +60,7 @@ module ActiveRecord # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) + s.gsub('\\', '\&\&').gsub("'", "''") # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. @@ -95,7 +95,7 @@ module ActiveRecord end def quoted_true - "TRUE".freeze + "TRUE" end def unquoted_true @@ -103,7 +103,7 @@ module ActiveRecord end def quoted_false - "FALSE".freeze + "FALSE" end def unquoted_false diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 529c9d8ca6..19ff780192 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -21,7 +21,7 @@ module ActiveRecord private def visit_AlterTable(o) - sql = "ALTER TABLE #{quote_table_name(o.name)} ".dup + sql = +"ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| accept col }.join(" ") sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ") sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ") @@ -29,17 +29,19 @@ module ActiveRecord def visit_ColumnDefinition(o) o.sql_type = type_to_sql(o.type, o.options) - column_sql = "#{quote_column_name(o.name)} #{o.sql_type}".dup + column_sql = +"#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end def visit_AddColumnDefinition(o) - "ADD #{accept(o.column)}".dup + +"ADD #{accept(o.column)}" end def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} ".dup + create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE " + create_sql << "IF NOT EXISTS " if o.if_not_exists + create_sql << "#{quote_table_name(o.name)} " statements = o.columns.map { |c| accept c } statements << accept(o.primary_keys) if o.primary_keys diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 582ac516c7..db489143af 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of @@ -256,15 +258,25 @@ module ActiveRecord class TableDefinition include ColumnMethods - attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment + attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys + attr_writer :indexes + deprecate :indexes= - def initialize(name, temporary = false, options = nil, as = nil, comment: nil) + def initialize( + name, + temporary: false, + if_not_exists: false, + options: nil, + as: nil, + comment: nil, + ** + ) @columns_hash = {} @indexes = [] @foreign_keys = [] @primary_keys = nil @temporary = temporary + @if_not_exists = if_not_exists @options = options @as = as @name = name @@ -348,8 +360,8 @@ module ActiveRecord # # create_table :taggings do |t| # t.references :tag, index: { name: 'index_taggings_on_tag_id' } - # t.references :tagger, polymorphic: true, index: true - # t.references :taggable, polymorphic: { default: 'Photo' } + # t.references :tagger, polymorphic: true + # t.references :taggable, polymorphic: { default: 'Photo' }, index: false # end def column(name, type, options = {}) name = name.to_s @@ -527,7 +539,9 @@ module ActiveRecord # # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) + index_options = options.delete(:index) @base.add_column(name, column_name, type, options) + index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options end # Checks to see if a column exists. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 3be0906f2a..38cfc3a241 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -205,19 +205,22 @@ module ActiveRecord # Set to true to drop the table before creating it. # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_not_exists</tt>] + # Set to true to avoid raising an error when the table already exists. + # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options. # # ====== Add a backend specific option to the generated SQL (MySQL) # - # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4') # # generates: # # CREATE TABLE suppliers ( # id bigint auto_increment PRIMARY KEY - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 # # ====== Rename the primary key column # @@ -287,8 +290,8 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, comment: nil, **options) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment + def create_table(table_name, **options) + td = create_table_definition(table_name, options) if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do @@ -317,7 +320,9 @@ module ActiveRecord end if supports_comments? && !supports_comments_in_create? - change_table_comment(table_name, comment) if comment.present? + if table_comment = options[:comment].presence + change_table_comment(table_name, table_comment) + end td.columns.each do |column| change_column_comment(table_name, column.name, column.comment) if column.comment.present? @@ -522,6 +527,9 @@ module ActiveRecord # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # * <tt>:collation</tt> - + # Specifies the collation for a <tt>:string</tt> or <tt>:text</tt> column. If not specified, the + # column will have the same collation as the table. # * <tt>:comment</tt> - # Specifies the comment for the column. This option is ignored by some backends. # @@ -599,6 +607,7 @@ module ActiveRecord # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. # In that case, +type+ and +options+ will be used by #add_column. + # Indexes on the column are automatically removed. def remove_column(table_name, column_name, type = nil, options = {}) execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end @@ -842,17 +851,17 @@ module ActiveRecord # [<tt>:null</tt>] # Whether the column allows nulls. Defaults to true. # - # ====== Create a user_id bigint column + # ====== Create a user_id bigint column without a index # - # add_reference(:products, :user) + # add_reference(:products, :user, index: false) # # ====== Create a user_id string column # # add_reference(:products, :user, type: :string) # - # ====== Create supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns # - # add_reference(:products, :supplier, polymorphic: true, index: true) + # add_reference(:products, :supplier, polymorphic: true) # # ====== Create a supplier_id column with a unique index # @@ -880,7 +889,7 @@ module ActiveRecord # # ====== Remove the reference # - # remove_reference(:products, :user, index: true) + # remove_reference(:products, :user, index: false) # # ====== Remove polymorphic reference # @@ -888,7 +897,7 @@ module ActiveRecord # # ====== Remove the reference with a foreign key # - # remove_reference(:products, :user, index: true, foreign_key: true) + # remove_reference(:products, :user, foreign_key: true) # def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options) if foreign_key @@ -980,11 +989,18 @@ module ActiveRecord # # remove_foreign_key :accounts, column: :owner_id # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, to_table: :owners + # # Removes the foreign key named +special_fk_name+ on the +accounts+ table. # # remove_foreign_key :accounts, name: :special_fk_name # - # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key + # with an addition of + # [<tt>:to_table</tt>] + # The name of the table that contains the referenced primary key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -1369,7 +1385,7 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) if versions.is_a?(Array) - sql = "INSERT INTO #{sm_table} (version) VALUES\n".dup + sql = +"INSERT INTO #{sm_table} (version) VALUES\n" sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") sql << ";\n\n" sql diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index b59df2fff7..0f2b1e85ff 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -91,12 +91,14 @@ module ActiveRecord end class Transaction #:nodoc: - attr_reader :connection, :state, :records, :savepoint_name + attr_reader :connection, :state, :records, :savepoint_name, :isolation_level def initialize(connection, options, run_commit_callbacks: false) @connection = connection @state = TransactionState.new @records = [] + @isolation_level = options[:isolation] + @materialized = false @joinable = options.fetch(:joinable, true) @run_commit_callbacks = run_commit_callbacks end @@ -105,6 +107,14 @@ module ActiveRecord records << record end + def materialize! + @materialized = true + end + + def materialized? + @materialized + end + def rollback_records ite = records.uniq while record = ite.shift @@ -127,7 +137,7 @@ module ActiveRecord record.committed! else # if not running callbacks, only adds the record to the parent transaction - record.add_to_transaction + connection.add_transaction_record(record) end end ensure @@ -141,24 +151,30 @@ module ActiveRecord end class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, parent_transaction, options, *args) - super(connection, options, *args) + def initialize(connection, savepoint_name, parent_transaction, *args) + super(connection, *args) parent_transaction.state.add_child(@state) - if options[:isolation] + if isolation_level raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end - connection.create_savepoint(@savepoint_name = savepoint_name) + + @savepoint_name = savepoint_name + end + + def materialize! + connection.create_savepoint(savepoint_name) + super end def rollback - connection.rollback_to_savepoint(savepoint_name) + connection.rollback_to_savepoint(savepoint_name) if materialized? @state.rollback! end def commit - connection.release_savepoint(savepoint_name) + connection.release_savepoint(savepoint_name) if materialized? @state.commit! end @@ -166,22 +182,23 @@ module ActiveRecord end class RealTransaction < Transaction - def initialize(connection, options, *args) - super - if options[:isolation] - connection.begin_isolated_db_transaction(options[:isolation]) + def materialize! + if isolation_level + connection.begin_isolated_db_transaction(isolation_level) else connection.begin_db_transaction end + + super end def rollback - connection.rollback_db_transaction + connection.rollback_db_transaction if materialized? @state.full_rollback! end def commit - connection.commit_db_transaction + connection.commit_db_transaction if materialized? @state.full_commit! end end @@ -190,6 +207,9 @@ module ActiveRecord def initialize(connection) @stack = [] @connection = connection + @has_unmaterialized_transactions = false + @materializing_transactions = false + @lazy_transactions_enabled = true end def begin_transaction(options = {}) @@ -203,11 +223,41 @@ module ActiveRecord run_commit_callbacks: run_commit_callbacks) end + transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled? @stack.push(transaction) + @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions? transaction end end + def disable_lazy_transactions! + materialize_transactions + @lazy_transactions_enabled = false + end + + def enable_lazy_transactions! + @lazy_transactions_enabled = true + end + + def lazy_transactions_enabled? + @lazy_transactions_enabled + end + + def materialize_transactions + return if @materializing_transactions + return unless @has_unmaterialized_transactions + + @connection.lock.synchronize do + begin + @materializing_transactions = true + @stack.each { |t| t.materialize! unless t.materialized? } + ensure + @materializing_transactions = false + end + @has_unmaterialized_transactions = false + end + end + def commit_transaction @connection.lock.synchronize do transaction = @stack.last diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index a4748dbeda..0fe868478c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -65,7 +65,7 @@ module ActiveRecord # Most of the methods in the adapter are useful during migrations. Most # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter - ADAPTER_NAME = "Abstract".freeze + ADAPTER_NAME = "Abstract" include ActiveSupport::Callbacks define_callbacks :checkout, :checkin @@ -80,6 +80,8 @@ module ActiveRecord attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner + set_callback :checkin, :after, :enable_lazy_transactions! + def self.type_cast_config_to_integer(config) if config.is_a?(Integer) config @@ -119,6 +121,16 @@ module ActiveRecord else @prepared_statements = false end + + @advisory_locks_enabled = self.class.type_cast_config_to_boolean( + config.fetch(:advisory_locks, true) + ) + + check_version + end + + def replica? + @config[:replica] || false end def migrations_paths # :nodoc: @@ -152,7 +164,7 @@ module ActiveRecord # this method must only be called while holding connection pool's mutex def lease if in_use? - msg = "Cannot lease connection, ".dup + msg = +"Cannot lease connection, " if @owner == Thread.current msg << "it is already leased by the current thread." else @@ -338,6 +350,10 @@ module ActiveRecord false end + def supports_lazy_transactions? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -346,6 +362,10 @@ module ActiveRecord def enable_extension(name) end + def advisory_locks_enabled? # :nodoc: + supports_advisory_locks? && @advisory_locks_enabled + end + # This is meant to be implemented by the adapters that support advisory # locks # @@ -449,6 +469,7 @@ module ActiveRecord # This is useful for when you need to call a proprietary method such as # PostgreSQL's lo_* methods. def raw_connection + disable_lazy_transactions! @connection end @@ -475,11 +496,7 @@ module ActiveRecord end def column_name_for_operation(operation, node) # :nodoc: - column_name_from_arel_node(node) - end - - def column_name_from_arel_node(node) # :nodoc: - visitor.accept(node, Arel::Collectors::SQLString.new).value + visitor.compile(node) end def default_index_type?(index) # :nodoc: @@ -487,6 +504,9 @@ module ActiveRecord end private + def check_version + end + def type_map @type_map ||= Type::TypeMap.new.tap do |mapping| initialize_type_map(mapping) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 9de8242a58..13c799b64a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -43,19 +43,17 @@ module ActiveRecord } class StatementPool < ConnectionAdapters::StatementPool # :nodoc: - private def dealloc(stmt) - stmt.close - end + private + + def dealloc(stmt) + stmt.close + end end def initialize(connection, logger, connection_options, config) super(connection, logger, config) @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - - if version < "5.1.10" - raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.1.10." - end end def version #:nodoc: @@ -74,6 +72,10 @@ module ActiveRecord !mariadb? && version >= "8.0.1" end + def supports_expression_index? + !mariadb? && version >= "8.0.13" + end + def supports_transaction_isolation? true end @@ -114,6 +116,14 @@ module ActiveRecord true end + def supports_longer_index_key_prefix? + if mariadb? + version >= "10.2.2" + else + version >= "5.7.9" + end + end + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 end @@ -127,7 +137,7 @@ module ActiveRecord end def index_algorithms - { default: "ALGORITHM = DEFAULT".dup, copy: "ALGORITHM = COPY".dup, inplace: "ALGORITHM = INPLACE".dup } + { default: +"ALGORITHM = DEFAULT", copy: +"ALGORITHM = COPY", inplace: +"ALGORITHM = INPLACE" } end # HELPER METHODS =========================================== @@ -180,6 +190,8 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.query(sql) @@ -211,18 +223,6 @@ module ActiveRecord execute "ROLLBACK" end - # In the simple case, MySQL allows us to place JOINs directly into the UPDATE - # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support - # these, we must use a subquery. - def join_to_update(update, select, key) # :nodoc: - if select.limit || select.offset || select.orders.any? - super - else - update.table select.source - update.wheres = select.constraints - end - end - def empty_insert_statement_value(primary_key = nil) "VALUES ()" end @@ -239,7 +239,7 @@ module ActiveRecord end # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. - # Charset defaults to utf8. + # Charset defaults to utf8mb4. # # Example: # create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin' @@ -248,8 +248,12 @@ module ActiveRecord def create_database(name, options = {}) if options[:collation] execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" + elsif options[:charset] + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}" + elsif supports_longer_index_key_prefix? + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`" else - execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" + raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns." end end @@ -376,7 +380,7 @@ module ActiveRecord def add_index(table_name, column_name, options = {}) #:nodoc: index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}".dup + sql = +"CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" execute add_sql_comment!(sql, comment) end @@ -531,6 +535,12 @@ module ActiveRecord end private + def check_version + if version < "5.5.8" + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8." + end + end + def combine_multi_statements(total_sql) total_sql.each_with_object([]) do |sql, total_sql_chunks| previous_packet = total_sql_chunks.last @@ -618,6 +628,8 @@ module ActiveRecord # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 + ER_NO_REFERENCED_ROW = 1216 + ER_ROW_IS_REFERENCED = 1217 ER_DO_NOT_HAVE_DEFAULT = 1364 ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 @@ -634,7 +646,7 @@ module ActiveRecord case error_number(exception) when ER_DUP_ENTRY RecordNotUnique.new(message) - when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 + when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) when ER_CANNOT_ADD_FOREIGN mismatched_foreign_key(message) @@ -717,20 +729,6 @@ module ActiveRecord [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] end - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subselect = select.clone - subselect.projections = [key] - - # Materialize subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.distinct unless select.limit || select.offset || select.orders.any? - - key_name = quote_column_name(key.name) - Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name)) - end - def supports_rename_index? mariadb? ? false : version >= "5.7.6" end @@ -769,7 +767,7 @@ module ActiveRecord # https://dev.mysql.com/doc/refman/5.7/en/set-names.html # (trailing comma because variable_assignments will always have content) if @config[:encoding] - encoding = "NAMES #{@config[:encoding]}".dup + encoding = +"NAMES #{@config[:encoding]}" encoding << " COLLATE #{@config[:collation]}" if @config[:collation] encoding << ", " end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 204691006c..2e7a78215a 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -114,8 +114,7 @@ module ActiveRecord class Resolver # :nodoc: attr_reader :configurations - # Accepts a hash two layers deep, keys on the first layer represent - # environments such as "production". Keys must be strings. + # Accepts a list of db config objects. def initialize(configurations) @configurations = configurations end @@ -136,33 +135,14 @@ module ActiveRecord # Resolver.new(configurations).resolve(:production) # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve(config) - if config - resolve_connection config - elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call - resolve_symbol_connection env.to_sym + def resolve(config_or_env, pool_name = nil) + if config_or_env + resolve_connection config_or_env, pool_name else raise AdapterNotSpecified end end - # Expands each key in @configurations hash into fully resolved hash - def resolve_all - config = configurations.dup - - if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) - end - - config.merge! env_config if env_config - - config.each do |key, value| - config[key] = resolve(value) if value - end - - config - end - # Returns an instance of ConnectionSpecification for a given adapter. # Accepts a hash one layer deep that contains all connection information. # @@ -176,7 +156,9 @@ module ActiveRecord # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # def spec(config) - spec = resolve(config).symbolize_keys + pool_name = config if config.is_a?(Symbol) + + spec = resolve(config, pool_name).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -211,7 +193,6 @@ module ActiveRecord end private - # Returns fully resolved connection, accepts hash, string or symbol. # Always returns a hash. # @@ -232,29 +213,42 @@ module ActiveRecord # Resolver.new({}).resolve_connection("postgresql://localhost/foo") # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve_connection(spec) - case spec + def resolve_connection(config_or_env, pool_name = nil) + case config_or_env when Symbol - resolve_symbol_connection spec + resolve_symbol_connection config_or_env, pool_name when String - resolve_url_connection spec + resolve_url_connection config_or_env when Hash - resolve_hash_connection spec + resolve_hash_connection config_or_env + else + resolve_connection config_or_env end end - # Takes the environment such as +:production+ or +:development+. + # Takes the environment such as +:production+ or +:development+ and a + # pool name the corresponds to the name given by the connection pool + # to the connection. That pool name is merged into the hash with the + # name key. + # # This requires that the @configurations was initialized with a key that # matches. # - # Resolver.new("production" => {}).resolve_symbol_connection(:production) - # # => {} + # configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0 + # @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250 + # @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}> + # ]> # - def resolve_symbol_connection(spec) - if config = configurations[spec.to_s] - resolve_connection(config).merge("name" => spec.to_s) + # Resolver.new(configurations).resolve_symbol_connection(:production, "primary") + # # => { "database" => "my_db" } + def resolve_symbol_connection(env_name, pool_name) + db_config = configurations.find_db_config(env_name) + + if db_config + resolve_connection(db_config.config).merge("name" => pool_name.to_s) else - raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") + raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}") end end diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb index 3dcb916d99..883747b84b 100644 --- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -10,7 +10,12 @@ module ActiveRecord super end - def visit_Arel_Nodes_In(*) + def visit_Arel_Nodes_In(o, collector) + @preparable = false + super + end + + def visit_Arel_Nodes_NotIn(o, collector) @preparable = false super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index d89eeb7f54..684c7042a7 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -29,6 +29,8 @@ module ActiveRecord end def exec_query(sql, name = "SQL", binds = [], prepare: false) + materialize_transactions + if without_prepared_statement?(binds) execute_and_free(sql, name) do |result| ActiveRecord::Result.new(result.fields, result.to_a) if result @@ -41,6 +43,8 @@ module ActiveRecord end def exec_delete(sql, name = nil, binds = []) + materialize_transactions + if without_prepared_statement?(binds) execute_and_free(sql, name) { @connection.affected_rows } else diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index be038403b8..75564a61d6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -5,7 +5,7 @@ module ActiveRecord module MySQL module Quoting # :nodoc: def quote_column_name(name) - @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`".freeze + @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" end def quote_table_name(name) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index c9ea653b77..82ed320617 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -17,7 +17,7 @@ module ActiveRecord end def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}".dup + change_column_sql = +"CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" add_column_position!(change_column_sql, column_options(o.column)) end @@ -64,7 +64,7 @@ module ActiveRecord def index_in_create(table_name, column_name, options) index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options) - add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})".dup, comment) + add_sql_comment!((+"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})"), comment) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index 1cf210d85b..4894fd1c08 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -35,13 +35,39 @@ module ActiveRecord ] end - indexes.last[-2] << row[:Column_name] - indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] - indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + if row[:Expression] + expression = row[:Expression] + expression = +"(#{expression})" unless expression.start_with?("(") + indexes.last[-2] << expression + indexes.last[-1][:expressions] ||= {} + indexes.last[-1][:expressions][expression] = expression + indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D" + else + indexes.last[-2] << row[:Column_name] + indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] + indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D" + end end end - indexes.map { |index| IndexDefinition.new(*index) } + indexes.map do |index| + options = index.last + + if expressions = options.delete(:expressions) + orders = options.delete(:orders) + lengths = options.delete(:lengths) + + columns = index[-2].map { |name| + [ name.to_sym, expressions[name] || +quote_column_name(name) ] + }.to_h + + index[-2] = add_options_for_index_columns( + columns, order: orders, length: lengths + ).values.join(", ") + end + + IndexDefinition.new(*index) + end end def remove_column(table_name, column_name, type = nil, options = {}) @@ -80,10 +106,13 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default]) - default, default_function = nil, field[:Default] - else - default, default_function = field[:Default], nil + default, default_function = field[:Default], nil + + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default) + default, default_function = nil, default + elsif type_metadata.extra == "DEFAULT_GENERATED" + default = +"(#{default})" unless default.start_with?("(") + default, default_function = nil, default end MySQL::Column.new( @@ -121,7 +150,7 @@ module ActiveRecord def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - sql = "SELECT table_name FROM information_schema.tables".dup + sql = +"SELECT table_name FROM information_schema.tables" sql << " WHERE table_schema = #{scope[:schema]}" sql << " AND table_name = #{scope[:name]}" if scope[:name] sql << " AND table_type = #{scope[:type]}" if scope[:type] diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 544d720428..9bdaa00336 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,7 +3,7 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", ">= 0.4.4", "< 0.6.0" +gem "mysql2", ">= 0.4.4" require "mysql2" module ActiveRecord @@ -14,7 +14,7 @@ module ActiveRecord config[:flags] ||= 0 if config[:flags].kind_of? Array - config[:flags].push "FOUND_ROWS".freeze + config[:flags].push "FOUND_ROWS" else config[:flags] |= Mysql2::Client::FOUND_ROWS end @@ -32,7 +32,7 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - ADAPTER_NAME = "Mysql2".freeze + ADAPTER_NAME = "Mysql2" include MySQL::DatabaseStatements @@ -58,6 +58,10 @@ module ActiveRecord true end + def supports_lazy_transactions? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 8db2a645af..6bd6b67165 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -58,6 +58,8 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do result_as_array @connection.async_exec(sql) @@ -70,6 +72,8 @@ module ActiveRecord # Note: the PG::Result object is manually memory managed; if you don't # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.async_exec(sql) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 26abeea7ed..6fbeaa2b9e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -33,7 +33,13 @@ module ActiveRecord def cast(value) if value.is_a?(::String) - value = @pg_decoder.decode(value) + value = begin + @pg_decoder.decode(value) + rescue TypeError + # malformed array string is treated as [], will raise in PG 2.0 gem + # this keeps a consistent implementation + [] + end end type_cast_array(value, :cast) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 231278c184..83c21ba6ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -16,12 +18,12 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row["oid"].to_i } - mapped, nodes = nodes.partition { |row| @store.key? row["typname"] } - ranges, nodes = nodes.partition { |row| row["typtype"] == "r".freeze } - enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze } - domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze } - arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze } - composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 } + mapped = nodes.extract! { |row| @store.key? row["typname"] } + ranges = nodes.extract! { |row| row["typtype"] == "r" } + enums = nodes.extract! { |row| row["typtype"] == "e" } + domains = nodes.extract! { |row| row["typtype"] == "d" } + arrays = nodes.extract! { |row| row["typinput"] == "array_in" } + composites = nodes.extract! { |row| row["typelem"].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e75202b0be..0895d06356 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -93,11 +93,11 @@ module ActiveRecord elsif value.hex? "X'#{value}'" end - when Float - if value.infinite? || value.nan? - "'#{value}'" - else + when Numeric + if value.finite? super + else + "'#{value}'" end when OID::Array::Data _quote(encode_array(value)) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 26b8113b0d..fae3ddbad4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -686,7 +686,7 @@ module ActiveRecord def change_column_sql(table_name, column_name, type, options = {}) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options) - sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup + sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" end @@ -700,6 +700,11 @@ module ActiveRecord sql end + def add_column_for_alter(table_name, column_name, type, options = {}) + return super unless options.key?(:comment) + [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }] + end + def change_column_for_alter(table_name, column_name, type, options = {}) sqls = [change_column_sql(table_name, column_name, type, options)] sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) @@ -708,7 +713,6 @@ module ActiveRecord sqls end - # Changes the default value of a table column. def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: column = column_for(table_name, column_name) @@ -753,7 +757,7 @@ module ActiveRecord scope = quoted_scope(name, type: type) scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table - sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup + sql = +"SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" sql << " WHERE n.nspname = #{scope[:schema]}" sql << " AND c.relname = #{scope[:name]}" if scope[:name] sql << " AND c.relkind IN (#{scope[:type]})" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index ffd3be26b0..cd69d28139 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -17,7 +17,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "".freeze) + super.gsub(/\[\]$/, "") end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index fdf6f75108..a11a786ec7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -4,6 +4,14 @@ gem "pg", ">= 0.18", "< 2.0" require "pg" +# Use async_exec instead of exec_params on pg versions before 1.1 +class ::PG::Connection # :nodoc: + unless self.public_method_defined?(:async_exec_params) + remove_method :exec_params + alias exec_params async_exec + end +end + require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/postgresql/column" @@ -35,9 +43,14 @@ module ActiveRecord valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] conn_params.slice!(*valid_conn_param_keys) - # The postgres drivers don't allow the creation of an unconnected PG::Connection object, - # so just pass a nil connection object for the time being. - ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) + conn = PG.connect(conn_params) + ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config) + rescue ::PG::Error => error + if error.message.include?("does not exist") + raise ActiveRecord::NoDatabaseError + else + raise + end end end @@ -70,7 +83,7 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See https://www.postgresql.org/docs/current/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - ADAPTER_NAME = "PostgreSQL".freeze + ADAPTER_NAME = "PostgreSQL" NATIVE_DATABASE_TYPES = { primary_key: "bigserial primary key", @@ -212,15 +225,11 @@ module ActiveRecord @local_tz = nil @max_identifier_length = nil - connect + configure_connection add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config[:statement_limit]) - if postgresql_version < 90100 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." - end - add_pg_decoders @type_map = Type::HashLookupTypeMap.new @@ -326,6 +335,10 @@ module ActiveRecord postgresql_version >= 90400 end + def supports_lazy_transactions? + true + end + def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer") @@ -398,6 +411,12 @@ module ActiveRecord end private + def check_version + if postgresql_version < 90100 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." + end + end + # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" @@ -436,7 +455,7 @@ module ActiveRecord end end - def get_oid_type(oid, fmod, column_name, sql_type = "".freeze) + def get_oid_type(oid, fmod, column_name, sql_type = "") if !type_map.key?(oid) load_additional_types([oid]) end @@ -525,13 +544,13 @@ module ActiveRecord # Quoted types when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m # The default 'now'::date is CURRENT_DATE - if $1 == "now".freeze && $2 == "date".freeze + if $1 == "now" && $2 == "date" nil else - $1.gsub("''".freeze, "'".freeze) + $1.gsub("''", "'") end # Boolean types - when "true".freeze, "false".freeze + when "true", "false" default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -597,15 +616,19 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) + materialize_transactions + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do - @connection.async_exec(sql, type_casted_binds) + @connection.exec_params(sql, type_casted_binds) end end end def exec_cache(sql, name, binds) + materialize_transactions + stmt_key = prepare_statement(sql) type_casted_binds = type_casted_binds(binds) @@ -639,7 +662,7 @@ module ActiveRecord # # Check here for more details: # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze + CACHED_PLAN_HEURISTIC = "cached plan must not change result type" def is_cached_plan_failure?(e) pgerror = e.cause code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) @@ -683,12 +706,6 @@ module ActiveRecord def connect @connection = PG.connect(@connection_parameters) configure_connection - rescue ::PG::Error => error - if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError - else - raise - end end # Configures the encoding, verbosity, schema search path, and time zone of the connection. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index abedf01f10..b2dcdb5373 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -13,7 +13,7 @@ module ActiveRecord end def quote_column_name(name) - @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}").freeze + @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") end def quoted_time(value) @@ -26,19 +26,19 @@ module ActiveRecord end def quoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1".freeze : "'t'".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1" : "'t'" end def unquoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t" end def quoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0".freeze : "'f'".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0" : "'f'" end def unquoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f" end private diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 24e7bc65fa..48277f0ae2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -21,19 +21,24 @@ module ActiveRecord WHERE name = #{quote(row['name'])} AND type = 'index' SQL - /\sWHERE\s+(?<where>.+)$/i =~ index_sql + /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?\z/i =~ index_sql columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col| col["name"] end - # Add info on sort order for columns (only desc order is explicitly specified, asc is - # the default) orders = {} - if index_sql # index_sql can be null in case of primary key indexes - index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column| - orders[order_column] = :desc - } + + if columns.any?(&:nil?) # index created with an expression + columns = expressions + else + # Add info on sort order for columns (only desc order is explicitly specified, + # asc is the default) + if index_sql # index_sql can be null in case of primary key indexes + index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column| + orders[order_column] = :desc + } + end end IndexDefinition.new( @@ -81,7 +86,7 @@ module ActiveRecord scope = quoted_scope(name, type: type) scope[:type] ||= "'table','view'" - sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'".dup + sql = +"SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" sql << " AND name = #{scope[:name]}" if scope[:name] sql << " AND type IN (#{scope[:type]})" sql diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index bee74dc33d..e0355a316b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -56,7 +56,7 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter - ADAPTER_NAME = "SQLite".freeze + ADAPTER_NAME = "SQLite" include SQLite3::Quoting include SQLite3::SchemaStatements @@ -105,11 +105,6 @@ module ActiveRecord @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - - if sqlite_version < "3.8.0" - raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." - end - configure_connection end @@ -125,6 +120,10 @@ module ActiveRecord true end + def supports_expression_index? + sqlite_version >= "3.9.0" + end + def requires_reloading? true end @@ -186,6 +185,10 @@ module ActiveRecord true end + def supports_lazy_transactions? + true + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity # :nodoc: @@ -212,6 +215,8 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = [], prepare: false) + materialize_transactions + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do @@ -252,6 +257,8 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.execute(sql) @@ -389,6 +396,12 @@ module ActiveRecord end private + def check_version + if sqlite_version < "3.8.0" + raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." + end + end + def initialize_type_map(m = type_map) super register_class_with_limit m, %r(int)i, SQLite3Integer @@ -409,12 +422,23 @@ module ActiveRecord def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" - caller = lambda { |definition| yield definition if block_given? } + foreign_keys = foreign_keys(table_name) + + caller = lambda do |definition| + rename = options[:rename] || {} + foreign_keys.each do |fk| + if column = rename[fk.options[:column]] + fk.options[:column] = column + end + definition.foreign_key(fk.to_table, fk.options) + end + + yield definition if block_given? + end transaction do disable_referential_integrity do - move_table(table_name, altered_table_name, - options.merge(temporary: true)) + move_table(table_name, altered_table_name, options.merge(temporary: true)) move_table(altered_table_name, table_name, &caller) end end @@ -446,6 +470,7 @@ module ActiveRecord primary_key: column_name == from_primary_key ) end + yield @definition if block_given? end copy_table_indexes(from, to, options[:rename] || {}) @@ -463,9 +488,12 @@ module ActiveRecord name = name[1..-1] end - to_column_names = columns(to).map(&:name) - columns = index.columns.map { |c| rename[c] || c }.select do |column| - to_column_names.include?(column) + columns = index.columns + if columns.is_a?(Array) + to_column_names = columns(to).map(&:name) + columns = columns.map { |c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end end unless columns.empty? @@ -549,7 +577,7 @@ module ActiveRecord column end else - basic_structure.to_hash + basic_structure.to_a end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index ee0e651912..777cb8a402 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -46,45 +46,111 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. - def establish_connection(config = nil) - raise "Anonymous class is not allowed." unless name + def establish_connection(config_or_env = nil) + config_hash = resolve_config_for_connection(config_or_env) + connection_handler.establish_connection(config_hash) + end - config ||= DEFAULT_ENV.call.to_sym - spec_name = self == Base ? "primary" : name - self.connection_specification_name = spec_name + # Connects a model to the databases specified. The +database+ keyword + # takes a hash consisting of a +role+ and a +database_key+. + # + # This will create a connection handler for switching between connections, + # look up the config hash using the +database_key+ and finally + # establishes a connection to that config. + # + # class AnimalsModel < ApplicationRecord + # self.abstract_class = true + # + # connects_to database: { writing: :primary, reading: :primary_replica } + # end + # + # Returns an array of established connections. + def connects_to(database: {}) + connections = [] - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) - spec = resolver.resolve(config).symbolize_keys - spec[:name] = spec_name + database.each do |role, database_key| + config_hash = resolve_config_for_connection(database_key) + handler = lookup_connection_handler(role.to_sym) - # use the primary config if a config is not passed in and - # it's a three tier config - spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] + connections << handler.establish_connection(config_hash) + end - connection_handler.establish_connection(spec) + connections end - class MergeAndResolveDefaultUrlConfig # :nodoc: - def initialize(raw_configurations) - @raw_config = raw_configurations.dup - @env = DEFAULT_ENV.call.to_s - end + # Connects to a database or role (ex writing, reading, or another + # custom role) for the duration of the block. + # + # If a role is passed, Active Record will look up the connection + # based on the requested role: + # + # ActiveRecord::Base.connected_to(role: :writing) do + # Dog.create! # creates dog using dog connection + # end + # + # ActiveRecord::Base.connected_to(role: :reading) do + # Dog.create! # throws exception because we're on a replica + # end + # + # ActiveRecord::Base.connected_to(role: :unknown_ode) do + # # raises exception due to non-existent role + # end + # + # For cases where you may want to connect to a database outside of the model, + # you can use +connected_to+ with a +database+ argument. The +database+ argument + # expects a symbol that corresponds to the database key in your config. + # + # This will connect to a new database for the queries inside the block. + # + # ActiveRecord::Base.connected_to(database: :animals_slow_replica) do + # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+ + # end + def connected_to(database: nil, role: nil, &blk) + if database && role + raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments." + elsif database + if database.is_a?(Hash) + role, database = database.first + role = role.to_sym + else + role = database.to_sym + end - # Returns fully resolved connection hashes. - # Merges connection information from `ENV['DATABASE_URL']` if available. - def resolve - ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all - end + config_hash = resolve_config_for_connection(database) + handler = lookup_connection_handler(role) - private - def config - @raw_config.dup.tap do |cfg| - if url = ENV["DATABASE_URL"] - cfg[@env] ||= {} - cfg[@env]["url"] ||= url - end - end + with_handler(role) do + handler.establish_connection(config_hash) + yield end + elsif role + with_handler(role.to_sym, &blk) + else + raise ArgumentError, "must provide a `database` or a `role`." + end + end + + def lookup_connection_handler(handler_key) # :nodoc: + connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new + end + + def with_handler(handler_key, &blk) # :nodoc: + handler = lookup_connection_handler(handler_key) + swap_connection_handler(handler, &blk) + end + + def resolve_config_for_connection(config_or_env) # :nodoc: + raise "Anonymous class is not allowed." unless name + + config_or_env ||= DEFAULT_ENV.call.to_sym + pool_name = self == Base ? "primary" : name + self.connection_specification_name = pool_name + + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) + config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys + config_hash[:name] = pool_name + + config_hash end # Returns the connection currently associated with the class. This can @@ -145,5 +211,14 @@ module ActiveRecord delegate :clear_active_connections!, :clear_reloadable_connections!, :clear_all_connections!, :flush_idle_connections!, to: :connection_handler + + private + + def swap_connection_handler(handler, &blk) # :nodoc: + old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler + yield + ensure + ActiveRecord::Base.connection_handler = old_handler + end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c983bc0d93..50f3087c51 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -2,6 +2,7 @@ require "active_support/core_ext/hash/indifferent_access" require "active_support/core_ext/string/filters" +require "active_support/parameter_filter" require "concurrent/map" module ActiveRecord @@ -26,7 +27,7 @@ module ActiveRecord ## # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. + # as an ActiveRecord::DatabaseConfigurations object. # # For example, the following database.yml... # @@ -40,22 +41,18 @@ module ActiveRecord # # ...would result in ActiveRecord::Base.configurations to look like this: # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } + # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>, + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production", + # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}> + # ]> def self.configurations=(config) - @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + @@configurations = ActiveRecord::DatabaseConfigurations.new(config) end self.configurations = {} - # Returns fully resolved configurations hash + # Returns fully resolved ActiveRecord::DatabaseConfigurations object def self.configurations @@configurations end @@ -125,8 +122,12 @@ module ActiveRecord mattr_accessor :belongs_to_required_by_default, instance_accessor: false + mattr_accessor :connection_handlers, instance_accessor: false, default: {} + class_attribute :default_connection_handler, instance_writer: false + self.filter_attributes = [] + def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler end @@ -136,9 +137,10 @@ module ActiveRecord end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new + self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } end - module ClassMethods # :nodoc: + module ClassMethods def initialize_find_by_cache # :nodoc: @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new } end @@ -215,7 +217,7 @@ module ActiveRecord generated_association_methods end - def generated_association_methods + def generated_association_methods # :nodoc: @generated_association_methods ||= begin mod = const_set(:GeneratedAssociationMethods, Module.new) private_constant :GeneratedAssociationMethods @@ -225,8 +227,20 @@ module ActiveRecord end end + # Returns columns which shouldn't be exposed while calling +#inspect+. + def filter_attributes + if defined?(@filter_attributes) + @filter_attributes + else + superclass.filter_attributes + end + end + + # Specifies columns which shouldn't be exposed while calling +#inspect+. + attr_writer :filter_attributes + # Returns a string like 'Post(id:integer, title:string, body:text)' - def inspect + def inspect # :nodoc: if self == Base super elsif abstract_class? @@ -242,7 +256,7 @@ module ActiveRecord end # Overwrite the default class equality method to provide support for decorated models. - def ===(object) + def ===(object) # :nodoc: object.is_a?(self) end @@ -326,34 +340,22 @@ module ActiveRecord # post = Post.allocate # post.init_with(coder) # post.title # => 'hello world' - def init_with(coder) + def init_with(coder, &block) coder = LegacyYamlAdapter.convert(self.class, coder) - @attributes = self.class.yaml_encoder.decode(coder) - - init_internals - - @new_record = coder["new_record"] - - self.class.define_attribute_methods - - yield self if block_given? - - _run_find_callbacks - _run_initialize_callbacks - - self + attributes = self.class.yaml_encoder.decode(coder) + init_with_attributes(attributes, coder["new_record"], &block) end ## - # Initializer used for instantiating objects that have been read from the - # database. +attributes+ should be an attributes object, and unlike the + # Initialize an empty model object from +attributes+. + # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. # # :nodoc: - def init_from_db(attributes) + def init_with_attributes(attributes, new_record = false) init_internals - @new_record = false + @new_record = new_record @attributes = attributes self.class.define_attribute_methods @@ -496,7 +498,14 @@ module ActiveRecord inspection = if defined?(@attributes) && @attributes self.class.attribute_names.collect do |name| if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" + attr = _read_attribute(name) + value = if attr.nil? + attr.inspect + else + attr = format_for_inspect(attr) + inspection_filter.filter_param(name, attr) + end + "#{name}: #{value}" end end.compact.join(", ") else @@ -512,15 +521,16 @@ module ActiveRecord return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes - column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } - pp.seplist(column_names, proc { pp.text "," }) do |column_name| - column_value = read_attribute(column_name) + attr_names = self.class.attribute_names.select { |name| has_attribute?(name) } + pp.seplist(attr_names, proc { pp.text "," }) do |attr_name| pp.breakable " " pp.group(1) do - pp.text column_name + pp.text attr_name pp.text ":" pp.breakable - pp.pp column_value + value = _read_attribute(attr_name) + value = inspection_filter.filter_param(attr_name, value) unless value.nil? + pp.pp value end end else @@ -571,5 +581,15 @@ module ActiveRecord def custom_inspect_method_defined? self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner end + + def inspection_filter + @inspection_filter ||= begin + mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED) + def mask.pretty_print(pp) + pp.text __getobj__ + end + ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask) + end + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index c7f0077a76..27c1b7a311 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -159,14 +159,11 @@ module ActiveRecord end private - - def _create_record(*) + def _create_record(attribute_names = self.attribute_names) id = super each_counter_cached_associations do |association| - if send(association.reflection.name) - association.increment_counters - end + association.increment_counters end id @@ -179,9 +176,7 @@ module ActiveRecord each_counter_cached_associations do |association| foreign_key = association.reflection.foreign_key.to_sym unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - if send(association.reflection.name) - association.decrement_counters - end + association.decrement_counters end end end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index ffeed45030..30cb0a27e7 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -1,63 +1,186 @@ # frozen_string_literal: true +require "active_record/database_configurations/database_config" +require "active_record/database_configurations/hash_config" +require "active_record/database_configurations/url_config" + module ActiveRecord - module DatabaseConfigurations # :nodoc: - class DatabaseConfig - attr_reader :env_name, :spec_name, :config + # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig + # objects (either a HashConfig or UrlConfig) that are constructed from the + # application's database configuration hash or url string. + class DatabaseConfigurations + attr_reader :configurations + delegate :any?, to: :configurations + + def initialize(configurations = {}) + @configurations = build_configs(configurations) + end - def initialize(env_name, spec_name, config) - @env_name = env_name - @spec_name = spec_name - @config = config + # Collects the configs for the environment and optionally the specification + # name passed in. To include replica configurations pass `include_replicas: true`. + # + # If a spec name is provided a single DatabaseConfig object will be + # returned, otherwise an array of DatabaseConfig objects will be + # returned that corresponds with the environment and type requested. + # + # Options: + # + # <tt>env_name:</tt> The environment name. Defaults to nil which will collect + # configs for all environments. + # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults + # to +nil+. + # <tt>include_replicas:</tt> Determines whether to include replicas in + # the returned list. Most of the time we're only iterating over the write + # connection (i.e. migrations don't need to run for the write and read connection). + # Defaults to +false+. + def configs_for(env_name: nil, spec_name: nil, include_replicas: false) + configs = env_with_configs(env_name) + + unless include_replicas + configs = configs.select do |db_config| + !db_config.replica? + end + end + + if spec_name + configs.find do |db_config| + db_config.spec_name == spec_name + end + else + configs end end - # Selects the config for the specified environment and specification name + # Returns the config hash that corresponds with the environment + # + # If the application has multiple databases `default_hash` will + # return the first config hash for the environment. + # + # { database: "my_db", adapter: "mysql2" } + def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s) + default = find_db_config(env) + default.config if default + end + alias :[] :default_hash + + # Returns a single DatabaseConfig object based on the requested environment. # - # For example if passed :development, and :animals it will select the database - # under the :development and :animals configuration level - def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: - configs_for(environment, configs).find do |db_config| - db_config.spec_name == specification_name + # If the application has multiple databases `find_db_config` will return + # the first DatabaseConfig for the environment. + def find_db_config(env) + configurations.find do |db_config| + db_config.env_name == env.to_s || + (db_config.for_current_env? && db_config.spec_name == env.to_s) end end - # Collects the configs for the environment passed in. + # Returns the DatabaseConfigurations object as a Hash. + def to_h + configs = configurations.reverse.inject({}) do |memo, db_config| + memo.merge(db_config.to_legacy_hash) + end + + Hash[configs.to_a.reverse] + end + + # Checks if the application's configurations are empty. # - # If a block is given returns the specification name and configuration - # otherwise returns an array of DatabaseConfig structs for the environment. - def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: - env_with_configs = db_configs(configs).select do |db_config| - db_config.env_name == env + # Aliased to blank? + def empty? + configurations.empty? + end + alias :blank? :empty? + + private + def env_with_configs(env = nil) + if env + configurations.select { |db_config| db_config.env_name == env } + else + configurations + end end - if block_given? - env_with_configs.each do |env_with_config| - yield env_with_config.spec_name, env_with_config.config + def build_configs(configs) + return configs.configurations if configs.is_a?(DatabaseConfigurations) + + build_db_config = configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name.to_s, "primary", config) + end.compact + + if url = ENV["DATABASE_URL"] + build_url_config(url, build_db_config) + else + build_db_config end - else - env_with_configs end - end - # Given an env, spec and config creates DatabaseConfig structs with - # each attribute set. - def self.walk_configs(env_name, spec_name, config) # :nodoc: - if config["database"] || config["url"] || config["adapter"] - DatabaseConfig.new(env_name, spec_name, config) - else - config.each_pair.map do |sub_spec_name, sub_config| - walk_configs(env_name, sub_spec_name, sub_config) + def walk_configs(env_name, spec_name, config) + case config + when String + build_db_config_from_string(env_name, spec_name, config) + when Hash + build_db_config_from_hash(env_name, spec_name, config.stringify_keys) end end - end - # Walks all the configs passed in and returns an array - # of DatabaseConfig structs for each configuration. - def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: - configs.each_pair.flat_map do |env_name, config| - walk_configs(env_name, "primary", config) + def build_db_config_from_string(env_name, spec_name, config) + begin + url = config + uri = URI.parse(url) + if uri.try(:scheme) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url) + end + rescue URI::InvalidURIError + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + end + end + + def build_db_config_from_hash(env_name, spec_name, config) + if url = config["url"] + config_without_url = config.dup + config_without_url.delete "url" + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url) + elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String }) + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + def build_url_config(url, configs) + env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + + if original_config = configs.find(&:for_current_env?) + if original_config.url_config? + configs + else + configs.map do |config| + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config) + end + end + else + configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)] + end + end + + def method_missing(method, *args, &blk) + if Hash.method_defined?(method) + ActiveSupport::Deprecation.warn \ + "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations." + end + + case method + when :each, :first + configurations.send(method, *args, &blk) + when :fetch + configs_for(env_name: args.first) + when :values + configurations.map(&:config) + else + super + end end - end end end diff --git a/activerecord/lib/active_record/database_configurations/database_config.rb b/activerecord/lib/active_record/database_configurations/database_config.rb new file mode 100644 index 0000000000..adc37cc439 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/database_config.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # ActiveRecord::Base.configurations will return either a HashConfig or + # UrlConfig respectively. It will never return a DatabaseConfig object, + # as this is the parent class for the types of database configuration objects. + class DatabaseConfig # :nodoc: + attr_reader :env_name, :spec_name + + def initialize(env_name, spec_name) + @env_name = env_name + @spec_name = spec_name + end + + def replica? + raise NotImplementedError + end + + def migrations_paths + raise NotImplementedError + end + + def url_config? + false + end + + def to_legacy_hash + { env_name => config } + end + + def for_current_env? + env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb new file mode 100644 index 0000000000..c176a62458 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/hash_config.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A HashConfig object is created for each database configuration entry that + # is created from a hash. + # + # A hash config: + # + # { "development" => { "database" => "db_name" } } + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 + # @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class HashConfig < DatabaseConfig + attr_reader :config + + def initialize(env_name, spec_name, config) + super(env_name, spec_name) + @config = config + end + + # Determines whether a database configuration is for a replica / readonly + # connection. If the `replica` key is present in the config, `replica?` will + # return +true+. + def replica? + config["replica"] + end + + # The migrations paths for a database configuration. If the + # `migrations_paths` key is present in the config, `migrations_paths` + # will return its value. + def migrations_paths + config["migrations_paths"] + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb new file mode 100644 index 0000000000..81917fc4c1 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A UrlConfig object is created for each database configuration + # entry that is created from a URL. This can either be a URL string + # or a hash with a URL in place of the config hash. + # + # A URL config: + # + # postgres://localhost/foo + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340 + # @env_name="default_env", @spec_name="primary", + # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"}, + # @url="postgres://localhost/foo"> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:url</tt> - The database URL. + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class UrlConfig < DatabaseConfig + attr_reader :url, :config + + def initialize(env_name, spec_name, url, config = {}) + super(env_name, spec_name) + @config = build_config(config, url) + @url = url + end + + def url_config? # :nodoc: + true + end + + # Determines whether a database configuration is for a replica / readonly + # connection. If the `replica` key is present in the config, `replica?` will + # return +true+. + def replica? + config["replica"] + end + + # The migrations paths for a database configuration. If the + # `migrations_paths` key is present in the config, `migrations_paths` + # will return its value. + def migrations_paths + config["migrations_paths"] + end + + private + def build_config(original_config, url) + if /^jdbc:/.match?(url) + hash = { "url" => url } + else + hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash + end + + if original_config[env_name] + original_config[env_name].merge(hash) + else + original_config.merge(hash) + end + end + end + end +end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 23ecb24542..3a600835e1 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -150,6 +150,7 @@ module ActiveRecord enum_prefix = definitions.delete(:_prefix) enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| + assert_valid_enum_definition_values(values) # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_s @@ -210,10 +211,24 @@ module ActiveRecord end end + def assert_valid_enum_definition_values(values) + unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) } + error_message = <<~MSG + Enum values #{values} must be either a hash, an array of symbols, or an array of strings. + MSG + raise ArgumentError, error_message + end + + if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?) + raise ArgumentError, "Enum label name must not be blank." + end + end + ENUM_CONFLICT_MESSAGE = \ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \ "this will generate a %{type} method \"%{method}\", which is already defined " \ "by %{source}." + private_constant :ENUM_CONFLICT_MESSAGE def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 7ccb938888..919e96cd7a 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -18,7 +18,7 @@ module ActiveRecord # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: str = queries.map do |sql, binds| - msg = "EXPLAIN for: #{sql}".dup + msg = +"EXPLAIN for: #{sql}" unless binds.empty? msg << " " msg << binds.map { |attr| render_bind(attr) }.inspect diff --git a/activerecord/lib/active_record/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb new file mode 100644 index 0000000000..fb23df6f45 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module ActiveRecord + class FixtureSet + class ModelMetadata # :nodoc: + def initialize(model_class) + @model_class = model_class + end + + def primary_key_name + @primary_key_name ||= @model_class && @model_class.primary_key + end + + def primary_key_type + @primary_key_type ||= @model_class && @model_class.type_for_attribute(@model_class.primary_key).type + end + + def has_primary_key_column? + @has_primary_key_column ||= primary_key_name && + @model_class.columns.any? { |col| col.name == primary_key_name } + end + + def timestamp_column_names + @timestamp_column_names ||= + %w(created_at created_on updated_at updated_on) & @model_class.column_names + end + + def inheritance_column_name + @inheritance_column_name ||= @model_class && @model_class.inheritance_column + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/render_context.rb b/activerecord/lib/active_record/fixture_set/render_context.rb new file mode 100644 index 0000000000..c90b5343dc --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/render_context.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# NOTE: This class has to be defined in compact style in +# order for rendering context subclassing to work correctly. +class ActiveRecord::FixtureSet::RenderContext # :nodoc: + def self.create_subclass + Class.new(ActiveRecord::FixtureSet.context_class) do + def get_binding + binding() + end + + def binary(path) + %(!!binary "#{Base64.strict_encode64(File.read(path))}") + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb new file mode 100644 index 0000000000..cb4726f1ee --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/table_row.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +module ActiveRecord + class FixtureSet + class TableRow # :nodoc: + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + + def primary_key_type + @association.klass.type_for_attribute(@association.klass.primary_key).type + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + + def join_table + @association.through_reflection.table_name + end + end + + def initialize(fixture, table_rows:, label:, now:) + @table_rows = table_rows + @label = label + @now = now + @row = fixture.to_hash + fill_row_model_attributes + end + + def to_hash + @row + end + + private + + def model_metadata + @table_rows.model_metadata + end + + def model_class + @table_rows.model_class + end + + def fill_row_model_attributes + return unless model_class + fill_timestamps + interpolate_label + generate_primary_key + resolve_enums + resolve_sti_reflections + end + + def reflection_class + @reflection_class ||= if @row.include?(model_metadata.inheritance_column_name) + @row[model_metadata.inheritance_column_name].constantize rescue model_class + else + model_class + end + end + + def fill_timestamps + # fill in timestamp columns if they aren't specified and the model is set to record_timestamps + if model_class.record_timestamps + model_metadata.timestamp_column_names.each do |c_name| + @row[c_name] = @now unless @row.key?(c_name) + end + end + end + + def interpolate_label + # interpolate the fixture label + @row.each do |key, value| + @row[key] = value.gsub("$LABEL", @label.to_s) if value.is_a?(String) + end + end + + def generate_primary_key + # generate a primary key if necessary + if model_metadata.has_primary_key_column? && !@row.include?(model_metadata.primary_key_name) + @row[model_metadata.primary_key_name] = ActiveRecord::FixtureSet.identify( + @label, model_metadata.primary_key_type + ) + end + end + + def resolve_enums + model_class.defined_enums.each do |name, values| + if @row.include?(name) + @row[name] = values.fetch(@row[name], @row[name]) + end + end + end + + def resolve_sti_reflections + # If STI is used, find the correct subclass for association reflection + reflection_class._reflections.each_value do |association| + case association.macro + when :belongs_to + # Do not replace association name with association foreign key if they are named the same + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + + if association.name.to_s != fk_name && value = @row.delete(association.name.to_s) + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + @row[association.foreign_type] = $1 + end + + fk_type = reflection_class.type_for_attribute(fk_name).type + @row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) + end + when :has_many + if association.options[:through] + add_join_records(HasManyThroughProxy.new(association)) + end + end + end + end + + def add_join_records(association) + # This is the case when the join table has no fixtures file + if (targets = @row.delete(association.name.to_s)) + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + joins = targets.map do |target| + { lhs_key => @row[model_metadata.primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } + end + @table_rows.tables[table_name].concat(joins) + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb new file mode 100644 index 0000000000..23814b6cb5 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/table_rows.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "active_record/fixture_set/table_row" +require "active_record/fixture_set/model_metadata" + +module ActiveRecord + class FixtureSet + class TableRows # :nodoc: + def initialize(table_name, model_class:, fixtures:, config:) + @model_class = model_class + + # track any join tables we need to insert later + @tables = Hash.new { |h, table| h[table] = [] } + + # ensure this table is loaded before any HABTM associations + @tables[table_name] = nil + + build_table_rows_from(table_name, fixtures, config) + end + + attr_reader :tables, :model_class + + def to_hash + @tables.transform_values { |rows| rows.map(&:to_hash) } + end + + def model_metadata + @model_metadata ||= ModelMetadata.new(model_class) + end + + private + + def build_table_rows_from(table_name, fixtures, config) + now = config.default_timezone == :utc ? Time.now.utc : Time.now + + @tables[table_name] = fixtures.map do |label, fixture| + TableRow.new( + fixture, + table_rows: self, + label: label, + now: now, + ) + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6e0f1a0dfb..1248ed00c5 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -7,6 +7,9 @@ require "set" require "active_support/dependencies" require "active_support/core_ext/digest/uuid" require "active_record/fixture_set/file" +require "active_record/fixture_set/render_context" +require "active_record/fixture_set/table_rows" +require "active_record/test_fixtures" require "active_record/errors" module ActiveRecord @@ -440,60 +443,6 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} } - def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - config.pluralize_table_names ? - fixture_set_name.singularize.camelize : - fixture_set_name.camelize - end - - def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - "#{ config.table_name_prefix }"\ - "#{ fixture_set_name.tr('/', '_') }"\ - "#{ config.table_name_suffix }".to_sym - end - - def self.reset_cache - @@all_cached_fixtures.clear - end - - def self.cache_for_connection(connection) - @@all_cached_fixtures[connection] - end - - def self.fixture_is_cached?(connection, table_name) - cache_for_connection(connection)[table_name] - end - - def self.cached_fixtures(connection, keys_to_fetch = nil) - if keys_to_fetch - cache_for_connection(connection).values_at(*keys_to_fetch) - else - cache_for_connection(connection).values - end - end - - def self.cache_fixtures(connection, fixtures_map) - cache_for_connection(connection).update(fixtures_map) - end - - def self.instantiate_fixtures(object, fixture_set, load_instances = true) - if load_instances - fixture_set.each do |fixture_name, fixture| - begin - object.instance_variable_set "@#{fixture_name}", fixture.find - rescue FixtureClassNotFound - nil - end - end - end - end - - def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each_value do |fixture_set| - instantiate_fixtures(object, fixture_set, load_instances) - end - end - cattr_accessor :all_loaded_fixtures, default: {} class ClassCache @@ -502,14 +451,16 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } + @class_names.delete_if do |klass_name, klass| + !insert_class(@class_names, klass_name, klass) + end end def [](fs_name) - @class_names.fetch(fs_name) { + @class_names.fetch(fs_name) do klass = default_fixture_model(fs_name, @config).safe_constantize insert_class(@class_names, fs_name, klass) - } + end end private @@ -528,38 +479,128 @@ module ActiveRecord end end - def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) - fixture_set_names = Array(fixture_set_names).map(&:to_s) - class_names = ClassCache.new class_names, config + class << self + def default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + config.pluralize_table_names ? + fixture_set_name.singularize.camelize : + fixture_set_name.camelize + end - # FIXME: Apparently JK uses this. - connection = block_given? ? yield : ActiveRecord::Base.connection + def default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + "#{ config.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ config.table_name_suffix }".to_sym + end - files_to_read = fixture_set_names.reject { |fs_name| - fixture_is_cached?(connection, fs_name) - } + def reset_cache + @@all_cached_fixtures.clear + end - unless files_to_read.empty? - fixtures_map = {} + def cache_for_connection(connection) + @@all_cached_fixtures[connection] + end + + def fixture_is_cached?(connection, table_name) + cache_for_connection(connection)[table_name] + end + + def cached_fixtures(connection, keys_to_fetch = nil) + if keys_to_fetch + cache_for_connection(connection).values_at(*keys_to_fetch) + else + cache_for_connection(connection).values + end + end - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - conn, - fs_name, + def cache_fixtures(connection, fixtures_map) + cache_for_connection(connection).update(fixtures_map) + end + + def instantiate_fixtures(object, fixture_set, load_instances = true) + return unless load_instances + fixture_set.each do |fixture_name, fixture| + begin + object.instance_variable_set "@#{fixture_name}", fixture.find + rescue FixtureClassNotFound + nil + end + end + end + + def instantiate_all_loaded_fixtures(object, load_instances = true) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) + end + end + + def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) + fixture_set_names = Array(fixture_set_names).map(&:to_s) + class_names = ClassCache.new class_names, config + + # FIXME: Apparently JK uses this. + connection = block_given? ? yield : ActiveRecord::Base.connection + + fixture_files_to_read = fixture_set_names.reject do |fs_name| + fixture_is_cached?(connection, fs_name) + end + + if fixture_files_to_read.any? + fixtures_map = read_and_insert( + fixtures_directory, + fixture_files_to_read, + class_names, + connection, + ) + cache_fixtures(connection, fixtures_map) + end + cached_fixtures(connection, fixture_set_names) + end + + # Returns a consistent, platform-independent identifier for +label+. + # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. + def identify(label, column_type = :integer) + if column_type == :uuid + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) + else + Zlib.crc32(label.to_s) % MAX_ID + end + end + + # Superclass for the evaluation contexts used by ERB fixtures. + def context_class + @context_class ||= Class.new + end + + private + + def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc: + fixtures_map = {} + fixture_sets = fixture_files.map do |fixture_set_name| + klass = class_names[fixture_set_name] + fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new + nil, + fixture_set_name, klass, - ::File.join(fixtures_directory, fs_name)) + ::File.join(fixtures_directory, fixture_set_name) + ) end + update_all_loaded_fixtures(fixtures_map) + + insert(fixture_sets, connection) - update_all_loaded_fixtures fixtures_map - fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } + fixtures_map + end + + def insert(fixture_sets, connection) # :nodoc: + fixture_sets_by_connection = fixture_sets.group_by do |fixture_set| + fixture_set.model_class&.connection || connection + end fixture_sets_by_connection.each do |conn, set| table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - set.each do |fs| - fs.table_rows.each do |table, rows| + set.each do |fixture_set| + fixture_set.table_rows.each do |table, rows| table_rows_for_connection[table].unshift(*rows) end end @@ -570,34 +611,16 @@ module ActiveRecord set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } end end - - cache_fixtures(connection, fixtures_map) end - cached_fixtures(connection, fixture_set_names) - end - # Returns a consistent, platform-independent identifier for +label+. - # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. - def self.identify(label, column_type = :integer) - if column_type == :uuid - Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) - else - Zlib.crc32(label.to_s) % MAX_ID + def update_all_loaded_fixtures(fixtures_map) # :nodoc: + all_loaded_fixtures.update(fixtures_map) end end - # Superclass for the evaluation contexts used by ERB fixtures. - def self.context_class - @context_class ||= Class.new - end - - def self.update_all_loaded_fixtures(fixtures_map) # :nodoc: - all_loaded_fixtures.update(fixtures_map) - end - attr_reader :table_name, :name, :fixtures, :model_class, :config - def initialize(connection, name, class_name, path, config = ActiveRecord::Base) + def initialize(_, name, class_name, path, config = ActiveRecord::Base) @name = name @path = path @config = config @@ -606,11 +629,7 @@ module ActiveRecord @fixtures = read_fixture_files(path) - @connection = connection - - @table_name = (model_class.respond_to?(:table_name) ? - model_class.table_name : - self.class.default_fixture_table_name(name, config)) + @table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config) end def [](x) @@ -632,152 +651,18 @@ module ActiveRecord # Returns a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows - now = config.default_timezone == :utc ? Time.now.utc : Time.now - # allow a standard key to be used for doing defaults in YAML fixtures.delete("DEFAULTS") - # track any join tables we need to insert later - rows = Hash.new { |h, table| h[table] = [] } - - rows[table_name] = fixtures.map do |label, fixture| - row = fixture.to_hash - - if model_class - # fill in timestamp columns if they aren't specified and the model is set to record_timestamps - if model_class.record_timestamps - timestamp_column_names.each do |c_name| - row[c_name] = now unless row.key?(c_name) - end - end - - # interpolate the fixture label - row.each do |key, value| - row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String) - end - - # generate a primary key if necessary - if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) - end - - # Resolve enums - model_class.defined_enums.each do |name, values| - if row.include?(name) - row[name] = values.fetch(row[name], row[name]) - end - end - - # If STI is used, find the correct subclass for association reflection - reflection_class = - if row.include?(inheritance_column_name) - row[inheritance_column_name].constantize rescue model_class - else - model_class - end - - reflection_class._reflections.each_value do |association| - case association.macro - when :belongs_to - # Do not replace association name with association foreign key if they are named the same - fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s - - if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - # support polymorphic belongs_to as "label (Type)" - row[association.foreign_type] = $1 - end - - fk_type = reflection_class.type_for_attribute(fk_name).type - row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) - end - when :has_many - if association.options[:through] - add_join_records(rows, row, HasManyThroughProxy.new(association)) - end - end - end - end - - row - end - rows - end - - class ReflectionProxy # :nodoc: - def initialize(association) - @association = association - end - - def join_table - @association.join_table - end - - def name - @association.name - end - - def primary_key_type - @association.klass.type_for_attribute(@association.klass.primary_key).type - end - end - - class HasManyThroughProxy < ReflectionProxy # :nodoc: - def rhs_key - @association.foreign_key - end - - def lhs_key - @association.through_reflection.foreign_key - end - - def join_table - @association.through_reflection.table_name - end + TableRows.new( + table_name, + model_class: model_class, + fixtures: fixtures, + config: config, + ).to_hash end private - def primary_key_name - @primary_key_name ||= model_class && model_class.primary_key - end - - def primary_key_type - @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type - end - - def add_join_records(rows, row, association) - # This is the case when the join table has no fixtures file - if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - column_type = association.primary_key_type - lhs_key = association.lhs_key - rhs_key = association.rhs_key - - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - rows[table_name].concat targets.map { |target| - { lhs_key => row[primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } - } - end - end - - def has_primary_key_column? - @has_primary_key_column ||= primary_key_name && - model_class.columns.any? { |c| c.name == primary_key_name } - end - - def timestamp_column_names - @timestamp_column_names ||= - %w(created_at created_on updated_at updated_on) & column_names - end - - def inheritance_column_name - @inheritance_column_name ||= model_class && model_class.inheritance_column - end - - def column_names - @column_names ||= @connection.columns(@table_name).collect(&:name) - end def model_class=(class_name) if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @@ -841,225 +726,9 @@ module ActiveRecord alias :to_hash :fixture def find - if model_class - model_class.unscoped do - model_class.find(fixture[model_class.primary_key]) - end - else - raise FixtureClassNotFound, "No class attached to find." - end - end - end -end - -module ActiveRecord - module TestFixtures - extend ActiveSupport::Concern - - def before_setup # :nodoc: - setup_fixtures - super - end - - def after_teardown # :nodoc: - super - teardown_fixtures - end - - included do - class_attribute :fixture_path, instance_writer: false - class_attribute :fixture_table_names, default: [] - class_attribute :fixture_class_names, default: {} - class_attribute :use_transactional_tests, default: true - class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances - class_attribute :pre_loaded_fixtures, default: false - class_attribute :config, default: ActiveRecord::Base - class_attribute :lock_threads, default: true - end - - module ClassMethods - # Sets the model class for a fixture when the class name cannot be inferred from the fixture name. - # - # Examples: - # - # set_fixture_class some_fixture: SomeModel, - # 'namespaced/fixture' => Another::Model - # - # The keys must be the fixture names, that coincide with the short paths to the fixture files. - def set_fixture_class(class_names = {}) - self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys) - end - - def fixtures(*fixture_set_names) - if fixture_set_names.first == :all - fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq - fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } - else - fixture_set_names = fixture_set_names.flatten.map(&:to_s) - end - - self.fixture_table_names |= fixture_set_names - setup_fixture_accessors(fixture_set_names) - end - - def setup_fixture_accessors(fixture_set_names = nil) - fixture_set_names = Array(fixture_set_names || fixture_table_names) - methods = Module.new do - fixture_set_names.each do |fs_name| - fs_name = fs_name.to_s - accessor_name = fs_name.tr("/", "_").to_sym - - define_method(accessor_name) do |*fixture_names| - force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - return_single_record = fixture_names.size == 1 - fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty? - - @fixture_cache[fs_name] ||= {} - - instances = fixture_names.map do |f_name| - f_name = f_name.to_s if f_name.is_a?(Symbol) - @fixture_cache[fs_name].delete(f_name) if force_reload - - if @loaded_fixtures[fs_name][f_name] - @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find - else - raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" - end - end - - return_single_record ? instances.first : instances - end - private accessor_name - end - end - include methods - end - - def uses_transaction(*methods) - @uses_transaction = [] unless defined?(@uses_transaction) - @uses_transaction.concat methods.map(&:to_s) - end - - def uses_transaction?(method) - @uses_transaction = [] unless defined?(@uses_transaction) - @uses_transaction.include?(method.to_s) - end - end - - def run_in_transaction? - use_transactional_tests && - !self.class.uses_transaction?(method_name) - end - - def setup_fixtures(config = ActiveRecord::Base) - if pre_loaded_fixtures && !use_transactional_tests - raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests" - end - - @fixture_cache = {} - @fixture_connections = [] - @@already_loaded_fixtures ||= {} - @connection_subscriber = nil - - # Load fixtures once and begin transaction. - if run_in_transaction? - if @@already_loaded_fixtures[self.class] - @loaded_fixtures = @@already_loaded_fixtures[self.class] - else - @loaded_fixtures = load_fixtures(config) - @@already_loaded_fixtures[self.class] = @loaded_fixtures - end - - # Begin transactions for connections already established - @fixture_connections = enlist_fixture_connections - @fixture_connections.each do |connection| - connection.begin_transaction joinable: false - connection.pool.lock_thread = true if lock_threads - end - - # When connections are established in the future, begin a transaction too - @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload| - spec_name = payload[:spec_name] if payload.key?(:spec_name) - - if spec_name - begin - connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name) - rescue ConnectionNotEstablished - connection = nil - end - - if connection && !@fixture_connections.include?(connection) - connection.begin_transaction joinable: false - connection.pool.lock_thread = true if lock_threads - @fixture_connections << connection - end - end - end - - # Load fixtures for every test. - else - ActiveRecord::FixtureSet.reset_cache - @@already_loaded_fixtures[self.class] = nil - @loaded_fixtures = load_fixtures(config) - end - - # Instantiate fixtures for every test if requested. - instantiate_fixtures if use_instantiated_fixtures - end - - def teardown_fixtures - # Rollback changes if a transaction is active. - if run_in_transaction? - ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber - @fixture_connections.each do |connection| - connection.rollback_transaction if connection.transaction_open? - connection.pool.lock_thread = false - end - @fixture_connections.clear - else - ActiveRecord::FixtureSet.reset_cache - end - - ActiveRecord::Base.clear_active_connections! - end - - def enlist_fixture_connections - ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection) - end - - private - def load_fixtures(config) - fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config) - Hash[fixtures.map { |f| [f.name, f] }] - end - - def instantiate_fixtures - if pre_loaded_fixtures - raise RuntimeError, "Load fixtures before instantiating them." if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? - ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) - else - raise RuntimeError, "Load fixtures before instantiating them." if @loaded_fixtures.nil? - @loaded_fixtures.each_value do |fixture_set| - ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?) - end - end - end - - def load_instances? - use_instantiated_fixtures != :no_instances - end - end -end - -class ActiveRecord::FixtureSet::RenderContext # :nodoc: - def self.create_subclass - Class.new ActiveRecord::FixtureSet.context_class do - def get_binding - binding() - end - - def binary(path) - %(!!binary "#{Base64.strict_encode64(File.read(path))}") + raise FixtureClassNotFound, "No class attached to find." unless model_class + model_class.unscoped do + model_class.find(fixture[model_class.primary_key]) end end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 6891c575c7..138fd1cf53 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,6 +55,10 @@ module ActiveRecord if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attributes) + if subclass.nil? && scope_attributes = current_scope&.scope_for_create + subclass = subclass_from_attributes(scope_attributes) + end + if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end @@ -176,7 +180,7 @@ module ActiveRecord # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) - if type_name.start_with?("::".freeze) + if type_name.start_with?("::") # If the type is prefixed with a scope operator then we assume that # the type_name is an absolute reference. ActiveSupport::Dependencies.constantize(type_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 6cf26a9792..456689ec6d 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -60,7 +60,7 @@ module ActiveRecord # the cache key will also include a version. # # Product.cache_versioning = false - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) if new_record? "#{model_name.cache_key}/new" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 7f096bb532..4a3a31fc95 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,7 +61,7 @@ module ActiveRecord end private - def _create_record(attribute_names = self.attribute_names, *) + def _create_record(attribute_names = self.attribute_names) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default @@ -165,7 +165,7 @@ module ActiveRecord def inherited(subclass) subclass.class_eval do is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } - decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type| LockingType.new(type) end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 5d1d15c94d..130ef8a330 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -14,9 +14,9 @@ module ActiveRecord # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do - # # select * from accounts where name = 'shugo' limit 1 for update - # shugo = Account.where("name = 'shugo'").lock(true).first - # yuko = Account.where("name = 'yuko'").lock(true).first + # # select * from accounts where name = 'shugo' limit 1 for update nowait + # shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo") + # yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko") # shugo.balance -= 100 # shugo.save! # yuko.balance += 100 diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 1ae6840921..6b84431343 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -4,6 +4,8 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] + class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new + def self.runtime=(value) ActiveRecord::RuntimeRegistry.sql_runtime = value end @@ -100,21 +102,15 @@ module ActiveRecord end def log_query_source - location = extract_query_source_location(caller_locations) - - if location - source = "#{location.path}:#{location.lineno}" - source = source.sub("#{::Rails.root}/", "") if defined?(::Rails.root) + source = extract_query_source_location(caller) + if source logger.debug(" ↳ #{source}") end end - RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/" - PATHS_TO_IGNORE = /\A(#{RAILS_GEM_ROOT}|#{RbConfig::CONFIG["rubylibdir"]})/ - def extract_query_source_location(locations) - locations.find { |line| line.absolute_path && !line.absolute_path.match?(PATHS_TO_IGNORE) } + backtrace_cleaner.clean(locations).first end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d68ed3cad9..6e5a610642 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -140,8 +140,8 @@ module ActiveRecord end class ConcurrentMigrationError < MigrationError #:nodoc: - DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze - RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running." + RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock" def initialize(message = DEFAULT_MESSAGE) super @@ -161,7 +161,7 @@ module ActiveRecord class ProtectedEnvironmentError < ActiveRecordError #:nodoc: def initialize(env = "production") - msg = "You are attempting to run a destructive action against your '#{env}' database.\n".dup + msg = +"You are attempting to run a destructive action against your '#{env}' database.\n" msg << "If you are sure you want to continue, run the same command with the environment variable:\n" msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" super(msg) @@ -170,7 +170,7 @@ module ActiveRecord class EnvironmentMismatchError < ActiveRecordError def initialize(current: nil, stored: nil) - msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup + msg = +"You are attempting to modify a database that was last run in `#{ stored }` environment.\n" msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" msg << " rails db:environment:set" @@ -678,15 +678,13 @@ module ActiveRecord if connection.respond_to? :revert connection.revert { yield } else - recorder = CommandRecorder.new(connection) + recorder = command_recorder @connection = recorder suppress_messages do connection.revert { yield } end @connection = recorder.delegate - recorder.commands.each do |cmd, args, block| - send(cmd, *args, &block) - end + recorder.replay(self) end end end @@ -891,7 +889,7 @@ module ActiveRecord source_migrations.each do |migration| source = File.binread(migration.filename) inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" - magic_comments = "".dup + magic_comments = +"" loop do # If we have a magic comment in the original migration, # insert our comment after the first newline(end of the magic comment line) @@ -962,6 +960,10 @@ module ActiveRecord yield end end + + def command_recorder + CommandRecorder.new(connection) + end end # MigrationProxy is used to defer loading of the actual migration classes @@ -1169,7 +1171,7 @@ module ActiveRecord def migrations_path=(path) ActiveSupport::Deprecation.warn \ - "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \ + "`ActiveRecord::Migrator.migrations_path=` is now deprecated and will be removed in Rails 6.0. " \ "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." self.migrations_paths = [path] end @@ -1299,7 +1301,7 @@ module ActiveRecord record_version_state_after_migrating(migration.version) end rescue => e - msg = "An error has occurred, ".dup + msg = +"An error has occurred, " msg << "this and " if use_transaction?(migration) msg << "all later migrations canceled:\n\n#{e}" raise StandardError, msg, e.backtrace @@ -1357,7 +1359,7 @@ module ActiveRecord end def use_advisory_lock? - Base.connection.supports_advisory_locks? + Base.connection.advisory_locks_enabled? end def with_advisory_lock diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 087632b10f..82f5121d94 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -108,11 +108,17 @@ module ActiveRecord yield delegate.update_table_definition(table_name, self) end + def replay(migration) + commands.each do |cmd, args, block| + migration.send(cmd, *args, &block) + end + end + private module StraightReversions # :nodoc: private - { transaction: :transaction, + { execute_block: :execute_block, create_table: :drop_table, create_join_table: :drop_join_table, @@ -133,6 +139,17 @@ module ActiveRecord include StraightReversions + def invert_transaction(args) + sub_recorder = CommandRecorder.new(delegate) + sub_recorder.revert { yield } + + invertions_proc = proc { + sub_recorder.replay(self) + } + + [:transaction, args, invertions_proc] + end + def invert_drop_table(args, &block) if args.size == 1 && block == nil raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)." @@ -214,11 +231,24 @@ module ActiveRecord end def invert_remove_foreign_key(args) - from_table, to_table, remove_options = args - raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + from_table, options_or_to_table, options_or_nil = args + + to_table = if options_or_to_table.is_a?(Hash) + options_or_to_table[:to_table] + else + options_or_to_table + end + + remove_options = if options_or_to_table.is_a?(Hash) + options_or_to_table.except(:to_table) + else + options_or_nil + end + + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? reversed_args = [from_table, to_table] - reversed_args << remove_options if remove_options + reversed_args << remove_options if remove_options.present? [:add_foreign_key, reversed_args] end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 0edaaa0cf9..8f6fcfcaea 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -16,6 +16,21 @@ module ActiveRecord V6_0 = Current class V5_2 < V6_0 + module CommandRecorder + def invert_transaction(args, &block) + [:transaction, args, block] + end + end + + private + + def command_recorder + recorder = super + class << recorder + prepend CommandRecorder + end + recorder + end end class V5_1 < V5_2 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 694ff85fa1..c50a420432 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -218,11 +218,11 @@ module ActiveRecord end def full_table_name_prefix #:nodoc: - (parents.detect { |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix + (module_parents.detect { |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end def full_table_name_suffix #:nodoc: - (parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix + (module_parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix end # The array of names of environments where destructive actions should be prohibited. By default, @@ -375,7 +375,7 @@ module ActiveRecord # default values when instantiating the Active Record object for this table. def column_defaults load_schema - @column_defaults ||= _default_attributes.to_hash + @column_defaults ||= _default_attributes.deep_dup.to_hash end def _default_attributes # :nodoc: @@ -388,6 +388,11 @@ module ActiveRecord @column_names ||= columns.map(&:name) end + def symbol_column_to_string(name_symbol) # :nodoc: + @symbol_column_to_string_name_hash ||= column_names.index_by(&:to_sym) + @symbol_column_to_string_name_hash[name_symbol] + end + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns @@ -477,6 +482,7 @@ module ActiveRecord def reload_schema_from_cache @arel_table = nil @column_names = nil + @symbol_column_to_string_name_hash = nil @attribute_types = nil @content_columns = nil @default_attributes = nil @@ -503,9 +509,9 @@ module ActiveRecord def compute_table_name if base_class? # Nested classes are prefixed with singular parent table name. - if parent < Base && !parent.abstract_class? - contained = parent.table_name - contained = contained.singularize if parent.pluralize_table_names + if module_parent < Base && !module_parent.abstract_class? + contained = module_parent.table_name + contained = contained.singularize if module_parent.pluralize_table_names contained += "_" end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 50767ee93f..8b9098df6c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -426,7 +426,7 @@ module ActiveRecord existing_record.assign_attributes(assignable_attributes) association(association_name).initialize_attributes(existing_record) else - method = "build_#{association_name}" + method = :"build_#{association_name}" if respond_to?(method) send(method, assignable_attributes) else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 05963e5546..7bf8d568df 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -70,17 +70,6 @@ module ActiveRecord instantiate_instance_of(klass, attributes, column_types, &block) end - # Given a class, an attributes hash, +instantiate_instance_of+ returns a - # new instance of the class. Accepts only keys as strings. - # - # This is private, don't call it. :) - # - # :nodoc: - def instantiate_instance_of(klass, attributes, column_types = {}, &block) - attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_from_db(attributes, &block) - end - # Updates an object (or multiple objects) and saves it to the database, if validations pass. # The resulting object is returned whether the object was saved successfully to the database or not. # @@ -216,6 +205,13 @@ module ActiveRecord end private + # Given a class, an attributes hash, +instantiate_instance_of+ returns a + # new instance of the class. Accepts only keys as strings. + def instantiate_instance_of(klass, attributes, column_types = {}, &block) + attributes = klass.attributes_builder.build_from_database(attributes, column_types) + klass.allocate.init_with_attributes(attributes, &block) + end + # Called by +instantiate+ to decide which class to use for a new # record instance. # @@ -483,15 +479,16 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end + id_in_database = self.id_in_database + attributes.each do |k, v| + write_attribute_without_type_cast(k, v) + end + affected_rows = self.class._update_record( attributes, self.class.primary_key => id_in_database ) - attributes.each do |k, v| - write_attribute_without_type_cast(k, v) - end - affected_rows == 1 end @@ -718,7 +715,6 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? @@ -737,10 +733,12 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names - attributes_values = attributes_with_values_for_create(attribute_names) + attribute_names = attributes_for_create(attribute_names) + + new_id = self.class._insert_record( + attributes_with_values(attribute_names) + ) - new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false @@ -761,6 +759,8 @@ module ActiveRecord @_association_destroy_exception = nil end + # The name of the method used to touch a +belongs_to+ association when the + # +:touch+ option is used. def belongs_to_touch_method :touch end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 009f412234..538659d6bd 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -77,6 +77,10 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.backtrace_cleaner" do + ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner } + end + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after ::ActionDispatch::Callbacks, @@ -84,6 +88,31 @@ module ActiveRecord end end + initializer "Check for cache versioning support" do + config.after_initialize do |app| + ActiveSupport.on_load(:active_record) do + if app.config.active_record.cache_versioning && Rails.cache + unless Rails.cache.class.try(:supports_cache_versioning?) + raise <<-end_error + +You're using a cache store that doesn't support native cache versioning. +Your best option is to upgrade to a newer version of #{Rails.cache.class} +that supports cache versioning (#{Rails.cache.class}.supports_cache_versioning? #=> true). + +Next best, switch to a different cache store that does support cache versioning: +https://guides.rubyonrails.org/caching_with_rails.html#cache-stores. + +To keep using the current cache store, you can turn off cache versioning entirely: + + config.active_record.cache_versioning = false + +end_error + end + end + end + end + end + initializer "active_record.check_schema_cache_dump" do if config.active_record.delete(:use_schema_cache_dump) config.after_initialize do |app| @@ -108,6 +137,14 @@ module ActiveRecord end end + initializer "active_record.define_attribute_methods" do |app| + config.after_initialize do + ActiveSupport.on_load(:active_record) do + descendants.each(&:define_attribute_methods) if app.config.eager_load + end + end + end + initializer "active_record.warn_on_records_fetched_greater_than" do if config.active_record.warn_on_records_fetched_greater_than ActiveSupport.on_load(:active_record) do @@ -131,21 +168,7 @@ module ActiveRecord initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configuration - - begin - establish_connection - rescue ActiveRecord::NoDatabaseError - warn <<-end_warning -Oops - You have a database configured, but it doesn't exist yet! - -Here's how to get started: - - 1. Configure your database in config/database.yml. - 2. Run `rails db:create` to create the database. - 3. Run `rails db:setup` to load your database schema. -end_warning - raise - end + establish_connection end end @@ -176,9 +199,7 @@ end_warning end initializer "active_record.set_executor_hooks" do - ActiveSupport.on_load(:active_record) do - ActiveRecord::QueryCache.install_executor_hooks - end + ActiveRecord::QueryCache.install_executor_hooks end initializer "active_record.add_watchable_files" do |app| @@ -231,5 +252,11 @@ MSG end end end + + initializer "active_record.set_filter_attributes" do + ActiveSupport.on_load(:active_record) do + self.filter_attributes += Rails.application.config.filter_parameters + end + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 8b7d18fb3d..475baa7559 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -26,7 +26,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Create #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) end end @@ -45,7 +45,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Drop #{spec_name} database for current environment" task spec_name => [:load_config, :check_protected_environments] do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) end end @@ -73,8 +73,8 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end db_namespace["_dump"].invoke @@ -99,7 +99,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Migrate #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end @@ -149,18 +149,21 @@ db_namespace = namespace :db do desc "Display status of migrations" task status: :load_config do - unless ActiveRecord::SchemaMigration.table_exists? - abort "Schema migrations table does not exist yet." + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate_status end + end - # output - puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" - puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" - puts "-" * 50 - ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| - puts "#{status.center(8)} #{version.ljust(14)} #{name}" + namespace :status do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Display status of migrations for #{spec_name} database" + task spec_name => :load_config do + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate_status + end end - puts end end @@ -274,11 +277,10 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end end @@ -298,15 +300,22 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." task dump: :load_config do - conn = ActiveRecord::Base.connection - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache( + ActiveRecord::Base.connection, + filename, + ) + end end desc "Clears a db/schema_cache.yml file." task clear: :load_config do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - rm_f filename, verbose: false + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + rm_f filename, verbose: false + end end end end @@ -314,11 +323,10 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) - ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) - + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename) if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information @@ -356,22 +364,30 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test") + end ensure if should_reconnect - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env)) end end end # desc "Recreate the test database from an existent structure.sql file" task load_structure: %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test") + end end # desc "Empty the test database" task purge: %w(load_config check_protected_environments) do - ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config) + end end # desc 'Load the test schema' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6d2f75a3ae..b2110f727c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -612,9 +612,21 @@ module ActiveRecord # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of - if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym + return unless can_find_inverse_of_automatically?(self) + inverse_name_candidates = + if options[:as] + [options[:as]] + else + active_record_name = active_record.name.demodulize + [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)] + end + + inverse_name_candidates.map! do |candidate| + ActiveSupport::Inflector.underscore(candidate).to_sym + end + + inverse_name_candidates.detect do |inverse_name| begin reflection = klass._reflect_on_association(inverse_name) rescue NameError @@ -623,9 +635,7 @@ module ActiveRecord reflection = false end - if valid_inverse_reflection?(reflection) - return inverse_name - end + valid_inverse_reflection?(reflection) end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d3e1eaa08..d5b6082d13 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -62,7 +62,7 @@ module ActiveRecord # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar def new(attributes = nil, &block) - scoping { klass.new(scope_for_create(attributes), &block) } + scoping { klass.new(attributes, &block) } end alias build new @@ -87,11 +87,7 @@ module ActiveRecord # users.create(name: nil) # validation on name # # => #<User id: nil, name: nil, ...> def create(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, &block) } - else - scoping { klass.create(scope_for_create(attributes), &block) } - end + scoping { klass.create(attributes, &block) } end # Similar to #create, but calls @@ -101,11 +97,7 @@ module ActiveRecord # Expects arguments in the same format as # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - scoping { klass.create!(scope_for_create(attributes), &block) } - end + scoping { klass.create!(attributes, &block) } end def first_or_create(attributes = nil, &block) # :nodoc: @@ -315,10 +307,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass - yield - ensure - klass.current_scope = previous unless @delegate_to_klass + @delegate_to_klass ? yield : klass._scoping(self) { yield } end def _exec_scope(*args, &block) # :nodoc: @@ -359,42 +348,49 @@ module ActiveRecord end stmt = Arel::UpdateManager.new - - stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates)) - stmt.table(table) - - if has_join_values? || offset_value - @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) + stmt.table(arel.join_sources.empty? ? table : arel.source) + stmt.key = arel_attribute(primary_key) + stmt.take(arel.limit) + stmt.offset(arel.offset) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints + + if updates.is_a?(Hash) + stmt.set _substitute_values(updates) else - stmt.key = arel_attribute(primary_key) - stmt.take(arel.limit) - stmt.order(*arel.orders) - stmt.wheres = arel.constraints + stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) end @klass.connection.update stmt, "#{@klass} Update All" end - def update(attributes) # :nodoc: - each { |record| record.update(attributes) } + def update(id = :all, attributes) # :nodoc: + if id == :all + each { |record| record.update(attributes) } + else + klass.update(id, attributes) + end end def update_counters(counters) # :nodoc: touch = counters.delete(:touch) - updates = counters.map do |counter_name, value| - operator = value < 0 ? "-" : "+" - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + updates = {} + counters.each do |counter_name, value| + attr = arel_attribute(counter_name) + bind = predicate_builder.build_bind_attribute(attr.name, value.abs) + expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0) + expr = value < 0 ? expr - bind : expr + bind + updates[counter_name] = expr.expr end if touch names = touch if touch != true touch_updates = klass.touch_attributes_with_time(*names) - updates << klass.sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? + updates.merge!(touch_updates) unless touch_updates.empty? end - update_all updates.join(", ") + update_all updates end # Touches all records in the current relation without instantiating records first with the updated_at/on attributes @@ -421,14 +417,12 @@ module ActiveRecord # Person.where(name: 'David').touch_all # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" def touch_all(*names, time: nil) - updates = touch_attributes_with_time(*names, time: time) - if klass.locking_enabled? - quoted_locking_column = connection.quote_column_name(klass.locking_column) - updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1" + names << { time: time } + update_counters(klass.locking_column => 1, touch: names) + else + update_all klass.touch_attributes_with_time(*names, time: time) end - - update_all(updates) end # Destroys the records by instantiating each @@ -484,13 +478,12 @@ module ActiveRecord end stmt = Arel::DeleteManager.new - stmt.from(table) - - if has_join_values? || has_limit_or_offset? - @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) - else - stmt.wheres = arel.constraints - end + stmt.from(arel.join_sources.empty? ? table : arel.source) + stmt.key = arel_attribute(primary_key) + stmt.take(arel.limit) + stmt.offset(arel.offset) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints affected = @klass.connection.delete(stmt, "#{@klass} Destroy") @@ -550,10 +543,8 @@ module ActiveRecord where_clause.to_h(relation_table_name) end - def scope_for_create(attributes = nil) - scope = where_values_hash.merge!(create_with_value.stringify_keys) - scope.merge!(attributes) if attributes - scope + def scope_for_create + where_values_hash.merge!(create_with_value.stringify_keys) end # Returns true if relation needs eager loading. @@ -636,9 +627,15 @@ module ActiveRecord end private - - def has_join_values? - joins_values.any? || left_outer_joins_values.any? + def _substitute_values(values) + values.map do |name, value| + attr = arel_attribute(name) + unless Arel.arel_node?(value) + type = klass.type_for_attribute(attr.name) + value = predicate_builder.build_bind_attribute(attr.name, type.cast(value)) + end + [attr, value] + end end def exec_queries(&block) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 40fe39fa9d..0fa5ba2e50 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -190,7 +190,7 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else - enforce_raw_sql_whitelist(column_names) + disallow_raw_sql!(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 488f71cdde..383dc1bf4b 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -17,7 +17,8 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - mangled_name = klass.name.gsub("::".freeze, "_".freeze) + include_relation_methods(delegate) + mangled_name = klass.name.gsub("::", "_") const_set mangled_name, delegate private_constant mangled_name @@ -29,6 +30,35 @@ module ActiveRecord child_class.initialize_relation_delegate_cache super end + + protected + def include_relation_methods(delegate) + superclass.include_relation_methods(delegate) unless base_class? + delegate.include generated_relation_methods + end + + private + def generated_relation_methods + @generated_relation_methods ||= Module.new.tap do |mod| + mod_name = "GeneratedRelationMethods" + const_set mod_name, mod + private_constant mod_name + end + end + + def generate_relation_method(method) + if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) + generated_relation_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { klass.#{method}(*args, &block) } + end + RUBY + else + generated_relation_methods.send(:define_method, method) do |*args, &block| + scoping { klass.public_send(method, *args, &block) } + end + end + end end extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index c5562c1ff0..afaa900442 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -338,14 +338,14 @@ module ActiveRecord name = @klass.name if ids.nil? - error = "Couldn't find #{name}".dup + error = +"Couldn't find #{name}" error << " with#{conditions}" if conditions raise RecordNotFound.new(error, name, key) elsif Array(ids).size == 1 error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup + error = +"Couldn't find all #{name.pluralize} with '#{key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})." error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids raise RecordNotFound.new(error, name, key, ids) @@ -363,7 +363,7 @@ module ActiveRecord case conditions when Array, Hash - relation.where!(conditions) + relation.where!(conditions) unless conditions.empty? else relation.where!(primary_key => conditions) unless conditions == :none end @@ -398,7 +398,7 @@ module ActiveRecord def limited_ids_for(relation) values = @klass.connection.columns_for_distinct( - connection.column_name_from_arel_node(arel_attribute(primary_key)), + connection.visitor.compile(arel_attribute(primary_key)), relation.order_values ) @@ -416,7 +416,7 @@ module ActiveRecord raise UnknownPrimaryKey.new(@klass) if primary_key.nil? expects_array = ids.first.kind_of?(Array) - return ids.first if expects_array && ids.first.empty? + return [] if expects_array && ids.first.empty? ids = ids.flatten.compact.uniq diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 10e4779ca4..4de7465128 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -152,10 +152,10 @@ module ActiveRecord def merge_multi_values if other.reordering_value # override any order specified in the original relation - relation.reorder! other.order_values + relation.reorder!(*other.order_values) elsif other.order_values.any? # merge in order_values from relation - relation.order! other.order_values + relation.order!(*other.order_values) end extensions = other.extensions - relation.extensions @@ -171,9 +171,7 @@ module ActiveRecord end def merge_clauses - if relation.from_clause.empty? && !other.from_clause.empty? - relation.from_clause = other.from_clause - end + relation.from_clause = other.from_clause if replace_from_clause? where_clause = relation.where_clause.merge(other.where_clause) relation.where_clause = where_clause unless where_clause.empty? @@ -181,6 +179,11 @@ module ActiveRecord having_clause = relation.having_clause.merge(other.having_clause) relation.having_clause = having_clause unless having_clause.empty? end + + def replace_from_clause? + relation.from_clause.empty? && !other.from_clause.empty? && + relation.klass.base_class == other.klass.base_class + end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index f734cd0ad8..b59ff912fe 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -27,7 +27,7 @@ module ActiveRecord key else key = key.to_s - key.split(".".freeze).first if key.include?(".".freeze) + key.split(".").first if key.include?(".") end end.compact end @@ -115,11 +115,11 @@ module ActiveRecord def convert_dot_notation_to_hash(attributes) dot_notation = attributes.select do |k, v| - k.include?(".".freeze) && !v.is_a?(Hash) + k.include?(".") && !v.is_a?(Hash) end dot_notation.each_key do |key| - table_name, column_name = key.split(".".freeze) + table_name, column_name = key.split(".") value = attributes.delete(key) attributes[table_name] ||= {} diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 64bf83e3c1..ee2ece1560 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: @@ -11,18 +13,18 @@ module ActiveRecord return attribute.in([]) if value.empty? values = value.map { |x| x.is_a?(Base) ? x.id : x } - nils, values = values.partition(&:nil?) - ranges, values = values.partition { |v| v.is_a?(Range) } + nils = values.extract!(&:nil?) + ranges = values.extract! { |v| v.is_a?(Range) } values_predicate = case values.length when 0 then NullPredicate when 1 then predicate_builder.build(attribute, values.first) else - bind_values = values.map do |v| + values.map! do |v| predicate_builder.build_bind_attribute(attribute.name, v) end - attribute.in(bind_values) + values.empty? ? NullPredicate : attribute.in(values) end unless nils.empty? diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 52405f21a1..eb80aab701 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -909,21 +909,18 @@ module ActiveRecord @arel ||= build_arel(aliases) end - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values.fetch(name, DEFAULT_VALUES[name]) - end - - protected + private + # Returns a relation value with a given name + def get_value(name) + @values.fetch(name, DEFAULT_VALUES[name]) + end # Sets the relation value with the given name - def set_value(name, value) # :nodoc: + def set_value(name, value) assert_mutability! @values[name] = value end - private - def assert_mutability! raise ImmutableRelation if @loaded raise ImmutableRelation if defined?(@arel) && @arel @@ -939,7 +936,7 @@ module ActiveRecord arel.having(having_clause.ast) unless having_clause.empty? if limit_value limit_attribute = ActiveModel::Attribute.with_cast_value( - "LIMIT".freeze, + "LIMIT", connection.sanitize_limit(limit_value), Type.default_value, ) @@ -947,7 +944,7 @@ module ActiveRecord end if offset_value offset_attribute = ActiveModel::Attribute.with_cast_value( - "OFFSET".freeze, + "OFFSET", offset_value.to_i, Type.default_value, ) @@ -1133,9 +1130,9 @@ module ActiveRecord end order_args.flatten! - @klass.enforce_raw_sql_whitelist( + @klass.disallow_raw_sql!( order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) validate_order_args(order_args) @@ -1188,8 +1185,9 @@ module ActiveRecord STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) + values = other.values STRUCTURAL_OR_METHODS.reject do |method| - get_value(method) == other.get_value(method) + get_value(method) == values.fetch(method, DEFAULT_VALUES[method]) end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 7f1c2fd7eb..da6d10b6ec 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -21,7 +21,7 @@ module ActiveRecord # ] # # # Get an array of hashes representing the result (column => value): - # result.to_hash + # result.to_a # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"}, # {"id" => 2, "title" => "title_2", "body" => "body_2"}, # ... @@ -65,9 +65,12 @@ module ActiveRecord end end - # Returns an array of hashes representing each row record. def to_hash - hash_rows + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActiveRecord::Result#to_hash` has been renamed to `to_a`. + `to_hash` is deprecated and will be removed in Rails 6.1. + MSG + to_a end alias :map! :map @@ -83,6 +86,8 @@ module ActiveRecord hash_rows end + alias :to_a :to_ary + def [](idx) hash_rows[idx] end @@ -140,6 +145,8 @@ module ActiveRecord # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map(&:-@) + length = columns.length + @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient @@ -148,8 +155,6 @@ module ActiveRecord hash = {} index = 0 - length = columns.length - while index < length hash[columns[index]] = row[index] index += 1 diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c6c268855e..3485d9e557 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -61,8 +61,8 @@ module ActiveRecord # # => "id ASC" def sanitize_sql_for_order(condition) if condition.is_a?(Array) && condition.first.to_s.include?("?") - enforce_raw_sql_whitelist([condition.first], - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + disallow_raw_sql!([condition.first], + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) # Ensure we aren't dealing with a subclass of String that might diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 01ac56570a..9eba1254a4 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -12,14 +12,6 @@ module ActiveRecord end module ClassMethods # :nodoc: - def current_scope(skip_inherited_scope = false) - ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) - end - - def current_scope=(scope) - ScopeRegistry.set_value_for(:current_scope, self, scope) - end - # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. def scope_attributes @@ -30,6 +22,15 @@ module ActiveRecord def scope_attributes? current_scope end + + private + def current_scope(skip_inherited_scope = false) + ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) + end + + def current_scope=(scope) + ScopeRegistry.set_value_for(:current_scope, self, scope) + end end def populate_with_current_scope_attributes # :nodoc: diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 8c612df27a..6caf9b3251 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -31,7 +31,14 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } def unscoped - block_given? ? relation.scoping { yield } : relation + block_given? ? _scoping(relation) { yield } : relation + end + + def _scoping(relation) # :nodoc: + previous, self.current_scope = current_scope(true), relation + yield + ensure + self.current_scope = previous end # Are there attributes associated with this scope? diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index a784001587..d5cc5db97e 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -24,13 +24,13 @@ module ActiveRecord # You can define a scope that applies to all finders using # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. def all - current_scope = self.current_scope + scope = current_scope - if current_scope - if self == current_scope.klass - current_scope.clone + if scope + if self == scope.klass + scope.clone else - relation.merge!(current_scope) + relation.merge!(scope) end else default_scoped @@ -38,9 +38,7 @@ module ActiveRecord end def scope_for_association(scope = relation) # :nodoc: - current_scope = self.current_scope - - if current_scope && current_scope.empty_scope? + if current_scope&.empty_scope? scope else default_scoped(scope) @@ -182,19 +180,19 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all - scope = scope._exec_scope(*args, &body) + scope = all._exec_scope(*args, &body) scope = scope.extending(extension) if extension scope end else singleton_class.send(:define_method, name) do |*args| - scope = all - scope = scope.scoping { body.call(*args) || scope } + scope = body.call(*args) || all scope = scope.extending(extension) if extension scope end end + + generate_relation_method(name) end private diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index b41d3504fd..1b1736dcab 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -44,7 +44,7 @@ module ActiveRecord def initialize(values) @values = values @indexes = values.each_with_index.find_all { |thing, i| - Arel::Nodes::BindParam === thing + Substitute === thing }.map(&:last) end @@ -56,6 +56,28 @@ module ActiveRecord end end + class PartialQueryCollector + def initialize + @parts = [] + @binds = [] + end + + def <<(str) + @parts << str + self + end + + def add_bind(obj) + @binds << obj + @parts << Substitute.new + self + end + + def value + [@parts, @binds] + end + end + def self.query(sql) Query.new(sql) end @@ -64,6 +86,10 @@ module ActiveRecord PartialQuery.new(values) end + def self.partial_query_collector + PartialQueryCollector.new + end + class Params # :nodoc: def bind; Substitute.new; end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index fd36c0abd2..27e401a756 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_record/database_configurations" + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -101,16 +103,21 @@ module ActiveRecord @env ||= Rails.env end + def spec + @spec ||= "primary" + end + def seed_loader @seed_loader ||= Rails.application end def current_config(options = {}) options.reverse_merge! env: env + options[:spec] ||= "primary" if options.has_key?(:config) @current_config = options[:config] else - @current_config ||= ActiveRecord::Base.configurations[options[:env]] + @current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).config end end @@ -122,7 +129,7 @@ module ActiveRecord $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error - $stderr.puts "Couldn't create database for #{configuration.inspect}" + $stderr.puts "Couldn't create '#{configuration['database']}' database. Please check your configuration." raise end @@ -135,8 +142,8 @@ module ActiveRecord end def for_each - databases = Rails.application.config.load_database_yaml - database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + databases = Rails.application.config.database_configuration + database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env) # if this is a single database application we don't want tasks for each primary database return if database_configs.count == 1 @@ -180,14 +187,31 @@ module ActiveRecord scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose? + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was end + def migrate_status + unless ActiveRecord::SchemaMigration.table_exists? + Kernel.abort "Schema migrations table does not exist yet." + end + + # output + puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" + puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" + puts "-" * 50 + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" + end + puts + end + def check_target_version if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"])) raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`" @@ -198,8 +222,8 @@ module ActiveRecord ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty? end - def charset_current(environment = env) - charset ActiveRecord::Base.configurations[environment] + def charset_current(environment = env, specification_name = spec) + charset ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config end def charset(*arguments) @@ -207,8 +231,8 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).charset end - def collation_current(environment = env) - collation ActiveRecord::Base.configurations[environment] + def collation_current(environment = env, specification_name = spec) + collation ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config end def collation(*arguments) @@ -289,6 +313,16 @@ module ActiveRecord ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) end + def cache_dump_filename(namespace) + filename = if namespace == "primary" + "schema_cache.yml" + else + "#{namespace}_schema_cache.yml" + end + + ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration, spec_name, env| load_schema(configuration, format, file, env, spec_name) @@ -298,7 +332,7 @@ module ActiveRecord def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.}.dup + message = +%{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails.root) Kernel.abort message end @@ -342,14 +376,15 @@ module ActiveRecord environments << "test" if environment == "development" environments.each do |env| - ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| - yield configuration, spec_name, env + ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config| + yield db_config.config, db_config.spec_name, env end end end def each_local_configuration - ActiveRecord::Base.configurations.each_value do |configuration| + ActiveRecord::Base.configurations.configs_for.each do |db_config| + configuration = db_config.config next unless configuration["database"] if local_database?(configuration) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index eddc6fa223..1c1b29b5e1 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -104,7 +104,7 @@ module ActiveRecord end def run_cmd_error(cmd, args, action) - msg = "failed to execute: `#{cmd}`\n".dup + msg = +"failed to execute: `#{cmd}`\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 533e6953a4..8acb11f75f 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -6,8 +6,8 @@ module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: DEFAULT_ENCODING = ENV["CHARSET"] || "utf8" - ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze - SQL_COMMENT_BEGIN = "--".freeze + ON_ERROR_STOP_1 = "ON_ERROR_STOP=1" + SQL_COMMENT_BEGIN = "--" delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -82,7 +82,7 @@ module ActiveRecord def structure_load(filename, extra_flags) set_psql_env - args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename] + args = ["-v", ON_ERROR_STOP_1, "-q", "-X", "-f", filename] args.concat(Array(extra_flags)) if extra_flags args << configuration["database"] run_cmd("psql", args, "loading") @@ -115,7 +115,7 @@ module ActiveRecord end def run_cmd_error(cmd, args, action) - msg = "failed to execute:\n".dup + msg = +"failed to execute:\n" msg << "#{cmd} #{args.join(' ')}\n\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index d0bad05176..a82cea80ca 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -67,7 +67,7 @@ module ActiveRecord end def run_cmd_error(cmd, args) - msg = "failed to execute:\n".dup + msg = +"failed to execute:\n" msg << "#{cmd} #{args.join(' ')}\n\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb index 5b4efe22c9..999830ba79 100644 --- a/activerecord/lib/active_record/test_databases.rb +++ b/activerecord/lib/active_record/test_databases.rb @@ -5,31 +5,32 @@ require "active_support/testing/parallelization" module ActiveRecord module TestDatabases # :nodoc: ActiveSupport::Testing::Parallelization.after_fork_hook do |i| - create_and_load_schema(i, spec_name: Rails.env) + create_and_load_schema(i, env_name: Rails.env) end - ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_| - drop(spec_name: Rails.env) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do + drop(env_name: Rails.env) end - def self.create_and_load_schema(i, spec_name:) + def self.create_and_load_schema(i, env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - - connection_spec["database"] += "-#{i}" - ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) - ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| + db_config.config["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name) + end ensure - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ActiveRecord::Base.establish_connection(Rails.env.to_sym) ENV["VERBOSE"] = old end - def self.drop(spec_name:) + def self.drop(env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end ensure ENV["VERBOSE"] = old end diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb new file mode 100644 index 0000000000..7b7b3f7112 --- /dev/null +++ b/activerecord/lib/active_record/test_fixtures.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +module ActiveRecord + module TestFixtures + extend ActiveSupport::Concern + + def before_setup # :nodoc: + setup_fixtures + super + end + + def after_teardown # :nodoc: + super + teardown_fixtures + end + + included do + class_attribute :fixture_path, instance_writer: false + class_attribute :fixture_table_names, default: [] + class_attribute :fixture_class_names, default: {} + class_attribute :use_transactional_tests, default: true + class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances + class_attribute :pre_loaded_fixtures, default: false + class_attribute :config, default: ActiveRecord::Base + class_attribute :lock_threads, default: true + end + + module ClassMethods + # Sets the model class for a fixture when the class name cannot be inferred from the fixture name. + # + # Examples: + # + # set_fixture_class some_fixture: SomeModel, + # 'namespaced/fixture' => Another::Model + # + # The keys must be the fixture names, that coincide with the short paths to the fixture files. + def set_fixture_class(class_names = {}) + self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys) + end + + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + raise StandardError, "No fixture path found. Please set `#{self}.fixture_path`." if fixture_path.blank? + fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq + fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } + else + fixture_set_names = fixture_set_names.flatten.map(&:to_s) + end + + self.fixture_table_names |= fixture_set_names + setup_fixture_accessors(fixture_set_names) + end + + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) + methods = Module.new do + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr("/", "_").to_sym + + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload + return_single_record = fixture_names.size == 1 + fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty? + + @fixture_cache[fs_name] ||= {} + + instances = fixture_names.map do |f_name| + f_name = f_name.to_s if f_name.is_a?(Symbol) + @fixture_cache[fs_name].delete(f_name) if force_reload + + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find + else + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" + end + end + + return_single_record ? instances.first : instances + end + private accessor_name + end + end + include methods + end + + def uses_transaction(*methods) + @uses_transaction = [] unless defined?(@uses_transaction) + @uses_transaction.concat methods.map(&:to_s) + end + + def uses_transaction?(method) + @uses_transaction = [] unless defined?(@uses_transaction) + @uses_transaction.include?(method.to_s) + end + end + + def run_in_transaction? + use_transactional_tests && + !self.class.uses_transaction?(method_name) + end + + def setup_fixtures(config = ActiveRecord::Base) + if pre_loaded_fixtures && !use_transactional_tests + raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests" + end + + @fixture_cache = {} + @fixture_connections = [] + @@already_loaded_fixtures ||= {} + @connection_subscriber = nil + + # Load fixtures once and begin transaction. + if run_in_transaction? + if @@already_loaded_fixtures[self.class] + @loaded_fixtures = @@already_loaded_fixtures[self.class] + else + @loaded_fixtures = load_fixtures(config) + @@already_loaded_fixtures[self.class] = @loaded_fixtures + end + + # Begin transactions for connections already established + @fixture_connections = enlist_fixture_connections + @fixture_connections.each do |connection| + connection.begin_transaction joinable: false + connection.pool.lock_thread = true if lock_threads + end + + # When connections are established in the future, begin a transaction too + @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload| + spec_name = payload[:spec_name] if payload.key?(:spec_name) + + if spec_name + begin + connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name) + rescue ConnectionNotEstablished + connection = nil + end + + if connection && !@fixture_connections.include?(connection) + connection.begin_transaction joinable: false + connection.pool.lock_thread = true if lock_threads + @fixture_connections << connection + end + end + end + + # Load fixtures for every test. + else + ActiveRecord::FixtureSet.reset_cache + @@already_loaded_fixtures[self.class] = nil + @loaded_fixtures = load_fixtures(config) + end + + # Instantiate fixtures for every test if requested. + instantiate_fixtures if use_instantiated_fixtures + end + + def teardown_fixtures + # Rollback changes if a transaction is active. + if run_in_transaction? + ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber + @fixture_connections.each do |connection| + connection.rollback_transaction if connection.transaction_open? + connection.pool.lock_thread = false + end + @fixture_connections.clear + else + ActiveRecord::FixtureSet.reset_cache + end + + ActiveRecord::Base.clear_active_connections! + end + + def enlist_fixture_connections + ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection) + end + + private + def load_fixtures(config) + fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config) + Hash[fixtures.map { |f| [f.name, f] }] + end + + def instantiate_fixtures + if pre_loaded_fixtures + raise RuntimeError, "Load fixtures before instantiating them." if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? + ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) + else + raise RuntimeError, "Load fixtures before instantiating them." if @loaded_fixtures.nil? + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?) + end + end + end + + def load_instances? + use_instantiated_fixtures != :no_instances + end + end +end diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index 7d04e1cac6..dab785738e 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -35,6 +35,11 @@ module Arel # :nodoc: all def self.star sql "*" end + + def self.arel_node?(value) + value.is_a?(Arel::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral) + end + ## Convenience Alias Node = Arel::Nodes::Node end diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb index d040d8598d..0533544993 100644 --- a/activerecord/lib/arel/collectors/composite.rb +++ b/activerecord/lib/arel/collectors/composite.rb @@ -24,8 +24,7 @@ module Arel # :nodoc: all [left.value, right.value] end - protected - + private attr_reader :left, :right end end diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb index 687d7fbf2f..c0e9fff399 100644 --- a/activerecord/lib/arel/collectors/plain_string.rb +++ b/activerecord/lib/arel/collectors/plain_string.rb @@ -4,7 +4,7 @@ module Arel # :nodoc: all module Collectors class PlainString def initialize - @str = "".dup + @str = +"" end def value diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb index c293a89a74..54e1e562c2 100644 --- a/activerecord/lib/arel/collectors/sql_string.rb +++ b/activerecord/lib/arel/collectors/sql_string.rb @@ -15,10 +15,6 @@ module Arel # :nodoc: all @bind_index += 1 self end - - def compile(bvs) - value - end end end end diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb index 3f40eec8a8..4b894bc4b1 100644 --- a/activerecord/lib/arel/collectors/substitute_binds.rb +++ b/activerecord/lib/arel/collectors/substitute_binds.rb @@ -21,8 +21,7 @@ module Arel # :nodoc: all delegate.value end - protected - + private attr_reader :quoter, :delegate end end diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb index 2def581009..fdba937d64 100644 --- a/activerecord/lib/arel/delete_manager.rb +++ b/activerecord/lib/arel/delete_manager.rb @@ -2,6 +2,8 @@ module Arel # :nodoc: all class DeleteManager < Arel::TreeManager + include TreeManager::StatementMethods + def initialize super @ast = Nodes::DeleteStatement.new @@ -12,14 +14,5 @@ module Arel # :nodoc: all @ast.relation = relation self end - - def take(limit) - @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit - self - end - - def wheres=(list) - @ast.wheres = list - end end end diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb index b828bc274e..83ec23e403 100644 --- a/activerecord/lib/arel/factory_methods.rb +++ b/activerecord/lib/arel/factory_methods.rb @@ -41,5 +41,9 @@ module Arel # :nodoc: all def lower(column) Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)] end + + def coalesce(*exprs) + Nodes::NamedFunction.new "COALESCE", exprs + end end end diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb index 53c5563d93..ba8340558a 100644 --- a/activerecord/lib/arel/nodes/bind_param.rb +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -3,7 +3,7 @@ module Arel # :nodoc: all module Nodes class BindParam < Node - attr_accessor :value + attr_reader :value def initialize(value) @value = value @@ -23,6 +23,10 @@ module Arel # :nodoc: all def nil? value.nil? end + + def boundable? + !value.respond_to?(:boundable?) || value.boundable? + end end end end diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb index eaac05e2f6..a419975335 100644 --- a/activerecord/lib/arel/nodes/delete_statement.rb +++ b/activerecord/lib/arel/nodes/delete_statement.rb @@ -3,8 +3,7 @@ module Arel # :nodoc: all module Nodes class DeleteStatement < Arel::Nodes::Node - attr_accessor :left, :right - attr_accessor :limit + attr_accessor :left, :right, :orders, :limit, :offset, :key alias :relation :left alias :relation= :left= @@ -15,6 +14,10 @@ module Arel # :nodoc: all super() @left = relation @right = wheres + @orders = [] + @limit = nil + @offset = nil + @key = nil end def initialize_copy(other) @@ -24,13 +27,17 @@ module Arel # :nodoc: all end def hash - [self.class, @left, @right].hash + [self.class, @left, @right, @orders, @limit, @offset, @key].hash end def eql?(other) self.class == other.class && self.left == other.left && - self.right == other.right + self.right == other.right && + self.orders == other.orders && + self.limit == other.limit && + self.offset == other.offset && + self.key == other.key end alias :== :eql? end diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb index 2b9b1e9828..8086102bde 100644 --- a/activerecord/lib/arel/nodes/node.rb +++ b/activerecord/lib/arel/nodes/node.rb @@ -8,16 +8,6 @@ module Arel # :nodoc: all include Arel::FactoryMethods include Enumerable - if $DEBUG - def _caller - @caller - end - - def initialize - @caller = caller.dup - end - end - ### # Factory method to create a Nodes::Not node that has the recipient of # the caller as a child. diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb index 2defe61974..73461ff683 100644 --- a/activerecord/lib/arel/nodes/select_core.rb +++ b/activerecord/lib/arel/nodes/select_core.rb @@ -3,13 +3,12 @@ module Arel # :nodoc: all module Nodes class SelectCore < Arel::Nodes::Node - attr_accessor :top, :projections, :wheres, :groups, :windows + attr_accessor :projections, :wheres, :groups, :windows attr_accessor :havings, :source, :set_quantifier def initialize super() @source = JoinSource.new nil - @top = nil # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier @set_quantifier = nil @@ -43,7 +42,7 @@ module Arel # :nodoc: all def hash [ - @source, @top, @set_quantifier, @projections, + @source, @set_quantifier, @projections, @wheres, @groups, @havings, @windows ].hash end @@ -51,7 +50,6 @@ module Arel # :nodoc: all def eql?(other) self.class == other.class && self.source == other.source && - self.top == other.top && self.set_quantifier == other.set_quantifier && self.projections == other.projections && self.wheres == other.wheres && diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb index a3c0045897..00639304e4 100644 --- a/activerecord/lib/arel/nodes/unary.rb +++ b/activerecord/lib/arel/nodes/unary.rb @@ -37,7 +37,6 @@ module Arel # :nodoc: all On Ordering RollUp - Top }.each do |name| const_set(name, Class.new(Unary)) end diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb index 5184b1180f..cfaa19e392 100644 --- a/activerecord/lib/arel/nodes/update_statement.rb +++ b/activerecord/lib/arel/nodes/update_statement.rb @@ -3,8 +3,7 @@ module Arel # :nodoc: all module Nodes class UpdateStatement < Arel::Nodes::Node - attr_accessor :relation, :wheres, :values, :orders, :limit - attr_accessor :key + attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key def initialize @relation = nil @@ -12,6 +11,7 @@ module Arel # :nodoc: all @values = [] @orders = [] @limit = nil + @offset = nil @key = nil end @@ -22,7 +22,7 @@ module Arel # :nodoc: all end def hash - [@relation, @wheres, @values, @orders, @limit, @key].hash + [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash end def eql?(other) @@ -32,6 +32,7 @@ module Arel # :nodoc: all self.values == other.values && self.orders == other.orders && self.limit == other.limit && + self.offset == other.offset && self.key == other.key end alias :== :eql? diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb index 22a04b00c6..a2b2838a3d 100644 --- a/activerecord/lib/arel/select_manager.rb +++ b/activerecord/lib/arel/select_manager.rb @@ -222,10 +222,8 @@ module Arel # :nodoc: all def take(limit) if limit @ast.limit = Nodes::Limit.new(limit) - @ctx.top = Nodes::Top.new(limit) else @ast.limit = nil - @ctx.top = nil end self end @@ -252,9 +250,9 @@ module Arel # :nodoc: all end private - def collapse(exprs, existing = nil) - exprs = exprs.unshift(existing.expr) if existing - exprs = exprs.compact.map { |expr| + def collapse(exprs) + exprs = exprs.compact + exprs.map! { |expr| if String === expr # FIXME: Don't do this automatically Arel.sql(expr) diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb index 686fcdf962..c40c68715a 100644 --- a/activerecord/lib/arel/table.rb +++ b/activerecord/lib/arel/table.rb @@ -104,8 +104,7 @@ module Arel # :nodoc: all !type_caster.nil? end - protected - + private attr_reader :type_caster end end diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb index ed47b09a37..0476399618 100644 --- a/activerecord/lib/arel/tree_manager.rb +++ b/activerecord/lib/arel/tree_manager.rb @@ -4,6 +4,40 @@ module Arel # :nodoc: all class TreeManager include Arel::FactoryMethods + module StatementMethods + def take(limit) + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + + def offset(offset) + @ast.offset = Nodes::Offset.new(Nodes.build_quoted(offset)) if offset + self + end + + def order(*expr) + @ast.orders = expr + self + end + + def key=(key) + @ast.key = Nodes.build_quoted(key) + end + + def key + @ast.key + end + + def wheres=(exprs) + @ast.wheres = exprs + end + + def where(expr) + @ast.wheres << expr + self + end + end + attr_reader :ast def initialize diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb index fe444343ba..a809dbb307 100644 --- a/activerecord/lib/arel/update_manager.rb +++ b/activerecord/lib/arel/update_manager.rb @@ -2,30 +2,14 @@ module Arel # :nodoc: all class UpdateManager < Arel::TreeManager + include TreeManager::StatementMethods + def initialize super @ast = Nodes::UpdateStatement.new @ctx = @ast end - def take(limit) - @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit - self - end - - def key=(key) - @ast.key = Nodes.build_quoted(key) - end - - def key - @ast.key - end - - def order(*expr) - @ast.orders = expr - self - end - ### # UPDATE +table+ def table(table) @@ -33,15 +17,6 @@ module Arel # :nodoc: all self end - def wheres=(exprs) - @ast.wheres = exprs - end - - def where(expr) - @ast.wheres << expr - self - end - def set(values) if String === values @ast.values = [values] diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb index bcf8f8f980..8f65d303ac 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -34,7 +34,6 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_Ordering :unary alias :visit_Arel_Nodes_Ascending :unary alias :visit_Arel_Nodes_Descending :unary - alias :visit_Arel_Nodes_Top :unary alias :visit_Arel_Nodes_UnqualifiedColumn :unary def function(o) @@ -136,12 +135,10 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_True :terminal alias :visit_Arel_Nodes_False :terminal alias :visit_BigDecimal :terminal - alias :visit_Bignum :terminal alias :visit_Class :terminal alias :visit_Date :terminal alias :visit_DateTime :terminal alias :visit_FalseClass :terminal - alias :visit_Fixnum :terminal alias :visit_Float :terminal alias :visit_Integer :terminal alias :visit_NilClass :terminal diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index d352b81914..9054f0159b 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -81,7 +81,6 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_Not :unary alias :visit_Arel_Nodes_Offset :unary alias :visit_Arel_Nodes_On :unary - alias :visit_Arel_Nodes_Top :unary alias :visit_Arel_Nodes_UnqualifiedColumn :unary alias :visit_Arel_Nodes_Preceding :unary alias :visit_Arel_Nodes_Following :unary @@ -212,7 +211,6 @@ module Arel # :nodoc: all alias :visit_TrueClass :visit_String alias :visit_FalseClass :visit_String alias :visit_Integer :visit_String - alias :visit_Fixnum :visit_String alias :visit_BigDecimal :visit_String alias :visit_Float :visit_String alias :visit_Symbol :visit_String diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index 9aedc51d15..d564e19089 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -12,13 +12,6 @@ module Arel # :nodoc: all private - # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate - # "select top 10 distinct first_name from users", which is invalid query! it should be - # "select distinct top 10 first_name from users" - def visit_Arel_Nodes_Top(o) - "" - end - def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) collector << "ROW_NUMBER() OVER (ORDER BY " inject_join(o.children, collector, ", ") << ") as _row_num" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 37bfb661f0..32f6705d04 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -9,20 +9,20 @@ module Arel # :nodoc: all collector << "( " end - collector = case o.left - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.left, collector, true - else - visit o.left, collector + case o.left + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.left, collector, true + else + visit o.left, collector end collector << " UNION " - collector = case o.right - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.right, collector, true - else - visit o.right, collector + case o.right + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.right, collector, true + else + visit o.right, collector end if suppress_parens @@ -37,6 +37,10 @@ module Arel # :nodoc: all visit o.expr, collector end + def visit_Arel_Nodes_UnqualifiedColumn(o, collector) + visit o.expr, collector + end + ### # :'( # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 @@ -52,28 +56,6 @@ module Arel # :nodoc: all super end - def visit_Arel_Nodes_UpdateStatement(o, collector) - collector << "UPDATE " - collector = visit o.relation, collector - - unless o.values.empty? - collector << " SET " - collector = inject_join o.values, collector, ", " - end - - unless o.wheres.empty? - collector << " WHERE " - collector = inject_join o.wheres, collector, " AND " - end - - unless o.orders.empty? - collector << " ORDER BY " - collector = inject_join o.orders, collector, ", " - end - - maybe_visit o.limit, collector - end - def visit_Arel_Nodes_Concat(o, collector) collector << " CONCAT(" visit o.left, collector @@ -82,6 +64,37 @@ module Arel # :nodoc: all collector << ") " collector end + + # In the simple case, MySQL allows us to place JOINs directly into the UPDATE + # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support + # these, we must use a subquery. + def prepare_update_statement(o) + if o.offset || has_join_sources?(o) && has_limit_or_offset_or_orders?(o) + super + else + o + end + end + alias :prepare_delete_statement :prepare_update_statement + + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. + def build_subselect(key, o) + subselect = super + + # Materialize subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + unless has_limit_or_offset_or_orders?(subselect) + core = subselect.cores.last + core.set_quantifier = Arel::Nodes::Distinct.new + end + + Nodes::SelectStatement.new.tap do |stmt| + core = stmt.cores.last + core.froms = Nodes::Grouping.new(subselect).as("__active_record_temp") + core.projections = [Arel.sql(quote_column_name(key.name))] + end + end end end end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 5986fd5576..8e56fb55a2 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -67,55 +67,39 @@ module Arel # :nodoc: all @connection = connection end - def compile(node, &block) - accept(node, Arel::Collectors::SQLString.new, &block).value + def compile(node, collector = Arel::Collectors::SQLString.new) + accept(node, collector).value end private def visit_Arel_Nodes_DeleteStatement(o, collector) - collector << "DELETE FROM " - collector = visit o.relation, collector - if o.wheres.any? - collector << WHERE - collector = inject_join o.wheres, collector, AND + o = prepare_delete_statement(o) + + if has_join_sources?(o) + collector << "DELETE " + visit o.relation.left, collector + collector << " FROM " + else + collector << "DELETE FROM " end + collector = visit o.relation, collector + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.orders, collector, " ORDER BY " maybe_visit o.limit, collector end - # FIXME: we should probably have a 2-pass visitor for this - def build_subselect(key, o) - stmt = Nodes::SelectStatement.new - core = stmt.cores.first - core.froms = o.relation - core.wheres = o.wheres - core.projections = [key] - stmt.limit = o.limit - stmt.orders = o.orders - stmt - end - def visit_Arel_Nodes_UpdateStatement(o, collector) - if o.orders.empty? && o.limit.nil? - wheres = o.wheres - else - wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] - end + o = prepare_update_statement(o) collector << "UPDATE " collector = visit o.relation, collector - unless o.values.empty? - collector << " SET " - collector = inject_join o.values, collector, ", " - end - - unless wheres.empty? - collector << " WHERE " - collector = inject_join wheres, collector, " AND " - end + collect_nodes_for o.values, collector, " SET " - collector + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.orders, collector, " ORDER BY " + maybe_visit o.limit, collector end def visit_Arel_Nodes_InsertStatement(o, collector) @@ -237,8 +221,6 @@ module Arel # :nodoc: all def visit_Arel_Nodes_SelectCore(o, collector) collector << "SELECT" - collector = maybe_visit o.top, collector - collector = maybe_visit o.set_quantifier, collector collect_nodes_for o.projections, collector, SPACE @@ -250,10 +232,7 @@ module Arel # :nodoc: all collect_nodes_for o.wheres, collector, WHERE, AND collect_nodes_for o.groups, collector, GROUP_BY - unless o.havings.empty? - collector << " HAVING " - inject_join o.havings, collector, AND - end + collect_nodes_for o.havings, collector, " HAVING ", AND collect_nodes_for o.windows, collector, WINDOW collector @@ -262,11 +241,7 @@ module Arel # :nodoc: all def collect_nodes_for(nodes, collector, spacer, connector = COMMA) unless nodes.empty? collector << spacer - len = nodes.length - 1 - nodes.each_with_index do |x, i| - collector = visit(x, collector) - collector << connector unless len == i - end + inject_join nodes, collector, connector end end @@ -321,10 +296,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Window(o, collector) collector << "(" - if o.partitions.any? - collector << "PARTITION BY " - collector = inject_join o.partitions, collector, ", " - end + collect_nodes_for o.partitions, collector, "PARTITION BY " if o.orders.any? collector << SPACE if o.partitions.any? @@ -405,11 +377,6 @@ module Arel # :nodoc: all visit o.expr, collector end - # FIXME: this does nothing on most databases, but does on MSSQL - def visit_Arel_Nodes_Top(o, collector) - collector - end - def visit_Arel_Nodes_Lock(o, collector) visit o.expr, collector end @@ -612,6 +579,10 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_In(o, collector) + if Array === o.right && !o.right.empty? + o.right.keep_if { |value| boundable?(value) } + end + if Array === o.right && o.right.empty? collector << "1=0" else @@ -622,6 +593,10 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_NotIn(o, collector) + if Array === o.right && !o.right.empty? + o.right.keep_if { |value| boundable?(value) } + end + if Array === o.right && o.right.empty? collector << "1=1" else @@ -644,7 +619,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Assignment(o, collector) case o.right - when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam + when Arel::Nodes::Node, Arel::Attributes::Attribute collector = visit o.left, collector collector << " = " visit o.right, collector @@ -739,8 +714,6 @@ module Arel # :nodoc: all end alias :visit_Arel_Nodes_SqlLiteral :literal - alias :visit_Bignum :literal - alias :visit_Fixnum :literal alias :visit_Integer :literal def quoted(o, a) @@ -823,6 +796,49 @@ module Arel # :nodoc: all } end + def boundable?(value) + !value.respond_to?(:boundable?) || value.boundable? + end + + def has_join_sources?(o) + o.relation.is_a?(Nodes::JoinSource) && !o.relation.right.empty? + end + + def has_limit_or_offset_or_orders?(o) + o.limit || o.offset || !o.orders.empty? + end + + # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work + # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in + # an UPDATE statement, so in the MySQL visitor we redefine this to do that. + def prepare_update_statement(o) + if o.key && (has_limit_or_offset_or_orders?(o) || has_join_sources?(o)) + stmt = o.clone + stmt.limit = nil + stmt.offset = nil + stmt.orders = [] + stmt.wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] + stmt.relation = o.relation.left if has_join_sources?(o) + stmt + else + o + end + end + alias :prepare_delete_statement :prepare_update_statement + + # FIXME: we should probably have a 2-pass visitor for this + def build_subselect(key, o) + stmt = Nodes::SelectStatement.new + core = stmt.cores.first + core.froms = o.relation + core.wheres = o.wheres + core.projections = [key] + stmt.limit = o.limit + stmt.offset = o.offset + stmt.orders = o.orders + stmt + end + def infix_value(o, collector, value) collector = visit o.left, collector collector << value diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4ceb502c5d..cbb88d571d 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -25,11 +25,24 @@ module ActiveRecord def db_migrate_path if defined?(Rails.application) && Rails.application - Rails.application.config.paths["db/migrate"].to_ary.first + configured_migrate_path || default_migrate_path else "db/migrate" end end + + def default_migrate_path + Rails.application.config.paths["db/migrate"].to_ary.first + end + + def configured_migrate_path + return unless database = options[:database] + config = ActiveRecord::Base.configurations.configs_for( + env_name: Rails.env, + spec_name: database, + ) + config&.migrations_paths + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index a07b00ef79..dd79bcf542 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -8,6 +8,7 @@ module ActiveRecord argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" class_option :primary_key_type, type: :string, desc: "The type for primary key" + class_option :database, type: :string, aliases: %i(db), desc: "The database for your migration. By default, the current environment's primary database is used." def create_migration_file set_local_assigns! diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 25e54f3ac8..eac504f9f1 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -14,6 +14,7 @@ module ActiveRecord class_option :parent, type: :string, desc: "The parent class for the generated model" class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns" class_option :primary_key_type, type: :string, desc: "The type for primary key" + class_option :database, type: :string, aliases: %i(db), desc: "The database for your model's migration. By default, the current environment's primary database is used." # creates the migration file for the model. def create_migration_file diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 59b99351d1..64c2b51f83 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "cases/helper" +require "support/connection_helper" require "models/book" require "models/post" require "models/author" @@ -10,6 +11,7 @@ module ActiveRecord class AdapterTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + @connection.materialize_transactions end ## @@ -225,7 +227,7 @@ module ActiveRecord post = Post.create!(title: "foo", body: "bar") expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}") result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new(nil).to_sql}", nil, [[nil, post.id]]) - assert_equal expected.to_hash, result.to_hash + assert_equal expected.to_a, result.to_a end def test_insert_update_delete_with_legacy_binds @@ -288,7 +290,7 @@ module ActiveRecord def test_log_invalid_encoding error = assert_raises RuntimeError do @connection.send :log, "SELECT 'ы' FROM DUAL" do - raise "ы".dup.force_encoding(Encoding::ASCII_8BIT) + raise (+"ы").force_encoding(Encoding::ASCII_8BIT) end end @@ -299,6 +301,34 @@ module ActiveRecord def test_supports_multi_insert_is_deprecated assert_deprecated { @connection.supports_multi_insert? } end + + def test_column_name_length_is_deprecated + assert_deprecated { @connection.column_name_length } + end + + def test_table_name_length_is_deprecated + assert_deprecated { @connection.table_name_length } + end + + def test_columns_per_table_is_deprecated + assert_deprecated { @connection.columns_per_table } + end + + def test_indexes_per_table_is_deprecated + assert_deprecated { @connection.indexes_per_table } + end + + def test_columns_per_multicolumn_index_is_deprecated + assert_deprecated { @connection.columns_per_multicolumn_index } + end + + def test_sql_query_length_is_deprecated + assert_deprecated { @connection.sql_query_length } + end + + def test_joins_per_query_is_deprecated + assert_deprecated { @connection.joins_per_query } + end end class AdapterForeignKeyTest < ActiveRecord::TestCase @@ -417,3 +447,27 @@ module ActiveRecord end end end + +if ActiveRecord::Base.connection.supports_advisory_locks? + class AdvisoryLocksEnabledTest < ActiveRecord::TestCase + include ConnectionHelper + + def test_advisory_locks_enabled? + assert ActiveRecord::Base.connection.advisory_locks_enabled? + + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(advisory_locks: false) + ) + + assert_not ActiveRecord::Base.connection.advisory_locks_enabled? + + ActiveRecord::Base.establish_connection( + orig_connection.merge(advisory_locks: true) + ) + + assert ActiveRecord::Base.connection.advisory_locks_enabled? + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 6fc9df5083..261fee13eb 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -106,7 +106,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt) assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index aa870349be..c32475c683 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -9,8 +9,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase repair_validations(CollationTest) def test_columns_include_collation_different_from_table - assert_equal "utf8_bin", CollationTest.columns_hash["string_cs_column"].collation - assert_equal "utf8_general_ci", CollationTest.columns_hash["string_ci_column"].collation + assert_equal "utf8mb4_bin", CollationTest.columns_hash["string_cs_column"].collation + assert_equal "utf8mb4_general_ci", CollationTest.columns_hash["string_ci_column"].collation end def test_case_sensitive diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index d0c57de65d..0bdbefdfb9 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -32,20 +32,20 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase end test "add column with charset and collation" do - @connection.add_column :charset_collations, :title, :string, charset: "utf8", collation: "utf8_bin" + @connection.add_column :charset_collations, :title, :string, charset: "utf8mb4", collation: "utf8mb4_bin" column = @connection.columns(:charset_collations).find { |c| c.name == "title" } assert_equal :string, column.type - assert_equal "utf8_bin", column.collation + assert_equal "utf8mb4_bin", column.collation end test "change column with charset and collation" do - @connection.add_column :charset_collations, :description, :string, charset: "utf8", collation: "utf8_unicode_ci" - @connection.change_column :charset_collations, :description, :text, charset: "utf8", collation: "utf8_general_ci" + @connection.add_column :charset_collations, :description, :string, charset: "utf8mb4", collation: "utf8mb4_unicode_ci" + @connection.change_column :charset_collations, :description, :text, charset: "utf8mb4", collation: "utf8mb4_general_ci" column = @connection.columns(:charset_collations).find { |c| c.name == "description" } assert_equal :text, column.type - assert_equal "utf8_general_ci", column.collation + assert_equal "utf8mb4_general_ci", column.collation end test "schema dump includes collation" do diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 726f58d58e..3103589186 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -104,8 +104,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_mysql_connection_collation_is_configured - assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection") - assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection") + assert_equal "utf8mb4_unicode_ci", @connection.show_variable("collation_connection") + assert_equal "utf8mb4_general_ci", ARUnit2Model.connection.show_variable("collation_connection") end def test_mysql_default_in_strict_mode @@ -170,6 +170,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_logs_name_show_variable + ActiveRecord::Base.connection.materialize_transactions + @subscriber.logged.clear @connection.show_variable "foo" assert_equal "SCHEMA", @subscriber.logged[0][1] end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 308ad1d854..afd422881b 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -4,6 +4,8 @@ require "cases/helper" class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def setup + ActiveRecord::Base.connection.materialize_transactions + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def execute(sql, name = nil) sql end end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 64bb6906cd..3988c2adca 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -49,7 +49,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_type_cast_binary_value - data = "\u001F\x8B".dup.force_encoding("BINARY") + data = (+"\u001F\x8B").force_encoding("BINARY") assert_equal(data, @type.deserialize(data)) end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 54b0dde7dc..70aa189893 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -15,8 +15,9 @@ module ActiveRecord def setup super @subscriber = SQLSubscriber.new - @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) @connection = ActiveRecord::Base.connection + @connection.materialize_transactions + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) end def teardown diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 61e75e772d..75e5aaed53 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -52,10 +52,10 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase def test_money_type_cast type = PostgresqlMoney.type_for_attribute("wealth") - assert_equal(12345678.12, type.cast("$12,345,678.12".dup)) - assert_equal(12345678.12, type.cast("$12.345.678,12".dup)) - assert_equal(-1.15, type.cast("-$1.15".dup)) - assert_equal(-2.25, type.cast("($2.25)".dup)) + assert_equal(12345678.12, type.cast(+"$12,345,678.12")) + assert_equal(12345678.12, type.cast(+"$12.345.678,12")) + assert_equal(-1.15, type.cast(+"-$1.15")) + assert_equal(-2.25, type.cast(+"($2.25)")) end def test_schema_dumping @@ -65,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase end def test_create_and_update_money - money = PostgresqlMoney.create(wealth: "987.65".dup) + money = PostgresqlMoney.create(wealth: +"987.65") assert_equal 987.65, money.wealth new_value = BigDecimal("123.45") diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index 0bcc214c24..ba477c63f4 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -101,7 +101,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase @connection.extend ProgrammerMistake assert_raises ArgumentError do - @connection.disable_referential_integrity {} + @connection.disable_referential_integrity { } end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index d1d4d545a3..89052019f8 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -87,7 +87,7 @@ module ActiveRecord def test_connection_no_db assert_raises(ArgumentError) do - Base.sqlite3_connection {} + Base.sqlite3_connection { } end end @@ -167,7 +167,7 @@ module ActiveRecord data binary ) eosql - str = "\x80".dup.force_encoding("ASCII-8BIT") + str = (+"\x80").force_encoding("ASCII-8BIT") binary = DualEncoding.new name: "いただきます!", data: str binary.save! assert_equal str, binary.data @@ -176,7 +176,7 @@ module ActiveRecord end def test_type_cast_should_not_mutate_encoding - name = "hello".dup.force_encoding(Encoding::ASCII_8BIT) + name = (+"hello").force_encoding(Encoding::ASCII_8BIT) Owner.create(name: name) assert_equal Encoding::ASCII_8BIT, name.encoding ensure @@ -345,6 +345,42 @@ module ActiveRecord end end + if ActiveRecord::Base.connection.supports_expression_index? + def test_expression_index + with_example_table do + @conn.add_index "ex", "max(id, number)", name: "expression" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "max(id, number)", index.columns + end + end + + def test_expression_index_with_where + with_example_table do + @conn.add_index "ex", "id % 10, max(id, number)", name: "expression", where: "id > 1000" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "id % 10, max(id, number)", index.columns + assert_equal "id > 1000", index.where + end + end + + def test_complicated_expression + with_example_table do + @conn.execute "CREATE INDEX expression ON ex (id % 10, (CASE WHEN number > 0 THEN max(id, number) END))WHERE(id > 1000)" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "id % 10, (CASE WHEN number > 0 THEN max(id, number) END)", index.columns + assert_equal "(id > 1000)", index.where + end + end + + def test_not_everything_an_expression + with_example_table do + @conn.add_index "ex", "id, max(id, number)", name: "expression" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "id, max(id, number)", index.columns + end + end + end + def test_primary_key with_example_table do assert_equal "id", @conn.primary_key("ex") diff --git a/activerecord/test/cases/arel/attributes/math_test.rb b/activerecord/test/cases/arel/attributes/math_test.rb index f3aabe4f60..41eea217c0 100644 --- a/activerecord/test/cases/arel/attributes/math_test.rb +++ b/activerecord/test/cases/arel/attributes/math_test.rb @@ -6,35 +6,35 @@ module Arel module Attributes class MathTest < Arel::Spec %i[* /].each do |math_operator| - it "average should be compatiable with #{math_operator}" do + it "average should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{ AVG("users"."id") #{math_operator} 2 } end - it "count should be compatiable with #{math_operator}" do + it "count should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{ COUNT("users"."id") #{math_operator} 2 } end - it "maximum should be compatiable with #{math_operator}" do + it "maximum should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{ MAX("users"."id") #{math_operator} 2 } end - it "minimum should be compatiable with #{math_operator}" do + it "minimum should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{ MIN("users"."id") #{math_operator} 2 } end - it "attribute node should be compatiable with #{math_operator}" do + it "attribute node should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{ "users"."id" #{math_operator} 2 @@ -43,35 +43,35 @@ module Arel end %i[+ - & | ^ << >>].each do |math_operator| - it "average should be compatiable with #{math_operator}" do + it "average should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{ (AVG("users"."id") #{math_operator} 2) } end - it "count should be compatiable with #{math_operator}" do + it "count should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{ (COUNT("users"."id") #{math_operator} 2) } end - it "maximum should be compatiable with #{math_operator}" do + it "maximum should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{ (MAX("users"."id") #{math_operator} 2) } end - it "minimum should be compatiable with #{math_operator}" do + it "minimum should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{ (MIN("users"."id") #{math_operator} 2) } end - it "attribute node should be compatiable with #{math_operator}" do + it "attribute node should be compatible with #{math_operator}" do table = Arel::Table.new :users (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{ ("users"."id" #{math_operator} 2) diff --git a/activerecord/test/cases/arel/collectors/sql_string_test.rb b/activerecord/test/cases/arel/collectors/sql_string_test.rb index 380573494d..443c7eb54b 100644 --- a/activerecord/test/cases/arel/collectors/sql_string_test.rb +++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb @@ -19,27 +19,21 @@ module Arel collect(node).value end - def ast_with_binds(bv) + def ast_with_binds table = Table.new(:users) manager = Arel::SelectManager.new table - manager.where(table[:age].eq(bv)) - manager.where(table[:name].eq(bv)) + manager.where(table[:age].eq(Nodes::BindParam.new("hello"))) + manager.where(table[:name].eq(Nodes::BindParam.new("world"))) manager.ast end def test_compile - bv = Nodes::BindParam.new(1) - collector = collect ast_with_binds bv - - sql = collector.compile ["hello", "world"] + sql = compile(ast_with_binds) assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql end def test_returned_sql_uses_utf8_encoding - bv = Nodes::BindParam.new(1) - collector = collect ast_with_binds bv - - sql = collector.compile ["hello", "world"] + sql = compile(ast_with_binds) assert_equal sql.encoding, Encoding::UTF_8 end end diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb index 17a5271039..0bad02f4d2 100644 --- a/activerecord/test/cases/arel/delete_manager_test.rb +++ b/activerecord/test/cases/arel/delete_manager_test.rb @@ -15,6 +15,7 @@ module Arel dm = Arel::DeleteManager.new dm.take 10 dm.from table + dm.key = table[:id] assert_match(/LIMIT 10/, dm.to_sql) end diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb index 6b10d0b612..5220950905 100644 --- a/activerecord/test/cases/arel/select_manager_test.rb +++ b/activerecord/test/cases/arel/select_manager_test.rb @@ -131,7 +131,7 @@ module Arel right = table.alias mgr = table.from mgr.join(right).on("omg") - mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg } + mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg } end it "converts to sqlliterals with multiple items" do @@ -139,7 +139,7 @@ module Arel right = table.alias mgr = table.from mgr.join(right).on("omg", "123") - mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 } + mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 } end end end @@ -278,7 +278,7 @@ module Arel @m2.where(table[:age].lt(99)) end - it "should interect two managers" do + it "should intersect two managers" do # FIXME should this intersect "managers" or "statements" ? # FIXME this probably shouldn't return a node node = @m1.intersect @m2 diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb index 3baccc7316..f94ad521d7 100644 --- a/activerecord/test/cases/arel/visitors/depth_first_test.rb +++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb @@ -33,7 +33,6 @@ module Arel Arel::Nodes::Ordering, Arel::Nodes::StringJoin, Arel::Nodes::UnqualifiedColumn, - Arel::Nodes::Top, Arel::Nodes::Limit, Arel::Nodes::Else, ].each do |klass| diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb index 98f3bab620..6b3c132f83 100644 --- a/activerecord/test/cases/arel/visitors/dot_test.rb +++ b/activerecord/test/cases/arel/visitors/dot_test.rb @@ -37,7 +37,6 @@ module Arel Arel::Nodes::Offset, Arel::Nodes::Ordering, Arel::Nodes::UnqualifiedColumn, - Arel::Nodes::Top, Arel::Nodes::Limit, ].each do |klass| define_method("test_#{klass.name.gsub('::', '_')}") do diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb index ba9cfcfc64..f7f2c76b6f 100644 --- a/activerecord/test/cases/arel/visitors/postgres_test.rb +++ b/activerecord/test/cases/arel/visitors/postgres_test.rb @@ -215,7 +215,7 @@ module Arel } end - it "should know how to generate paranthesis when supplied with many Dimensions" do + it "should know how to generate parenthesis when supplied with many Dimensions" do dim1 = Arel::Nodes::GroupingElement.new(@table[:name]) dim2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]]) node = Arel::Nodes::Cube.new([dim1, dim2]) @@ -241,7 +241,7 @@ module Arel } end - it "should know how to generate paranthesis when supplied with many Dimensions" do + it "should know how to generate parenthesis when supplied with many Dimensions" do group1 = Arel::Nodes::GroupingElement.new(@table[:name]) group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]]) node = Arel::Nodes::GroupingSet.new([group1, group2]) @@ -267,7 +267,7 @@ module Arel } end - it "should know how to generate paranthesis when supplied with many Dimensions" do + it "should know how to generate parenthesis when supplied with many Dimensions" do group1 = Arel::Nodes::GroupingElement.new(@table[:name]) group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]]) node = Arel::Nodes::RollUp.new([group1, group2]) diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb index ce836eded7..b6426a211e 100644 --- a/activerecord/test/cases/arel/visitors/to_sql_test.rb +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -221,7 +221,7 @@ module Arel sql.must_be_like "foo AS bar" end - it "should visit_Bignum" do + it "should visit_Integer" do compile 8787878092 end @@ -427,7 +427,7 @@ module Arel compile(node).must_equal %(("products"."price" - 7)) end - it "should handle Concatination" do + it "should handle Concatenation" do table = Table.new(:users) node = table[:name].concat(table[:name]) compile(node).must_equal %("users"."name" || "users"."name") diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 0cc4ed7127..93dd427951 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -367,6 +367,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal "ODEGY", odegy_account.reload_firm.name end + def test_reload_the_belonging_object_with_query_cache + odegy_account_id = accounts(:odegy_account).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy_account = Account.find(odegy_account_id) + + # Populate the cache with a second query + odegy_account.firm + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the firm again, populating the cache with a query + assert_queries(1) { odegy_account.reload_firm } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Account.find(odegy_account_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil @@ -452,15 +476,39 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_counter_with_assigning_nil - post = Post.find(1) - comment = Comment.find(1) + topic = Topic.create!(title: "debate") + reply = Reply.create!(title: "blah!", content: "world around!", topic: topic) + + assert_equal topic.id, reply.parent_id + assert_equal 1, topic.reload.replies.size - assert_equal post.id, comment.post_id - assert_equal 2, Post.find(post.id).comments.size + reply.topic = nil + reply.reload - comment.post = nil + assert_equal topic.id, reply.parent_id + assert_equal 1, topic.reload.replies.size + + reply.topic = nil + reply.save! - assert_equal 1, Post.find(post.id).comments.size + assert_equal 0, topic.reload.replies.size + end + + def test_belongs_to_counter_with_assigning_new_object + topic = Topic.create!(title: "debate") + reply = Reply.create!(title: "blah!", content: "world around!", topic: topic) + + assert_equal topic.id, reply.parent_id + assert_equal 1, topic.reload.replies_count + + topic2 = reply.build_topic(title: "debate2") + reply.save! + + assert_not_equal topic.id, reply.parent_id + assert_equal topic2.id, reply.parent_id + + assert_equal 0, topic.reload.replies_count + assert_equal 1, topic2.reload.replies_count end def test_belongs_to_with_primary_key_counter @@ -485,11 +533,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 0, debate2.reload.replies_count reply.topic_with_primary_key = debate2 + reply.save! assert_equal 0, debate.reload.replies_count assert_equal 1, debate2.reload.replies_count reply.topic_with_primary_key = nil + reply.save! assert_equal 0, debate.reload.replies_count assert_equal 0, debate2.reload.replies_count @@ -516,11 +566,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic2.id).replies.size reply1.topic = nil + reply1.save! assert_equal 0, Topic.find(topic1.id).replies.size assert_equal 0, Topic.find(topic2.id).replies.size reply1.topic = topic1 + reply1.save! assert_equal 1, Topic.find(topic1.id).replies.size assert_equal 0, Topic.find(topic2.id).replies.size @@ -550,7 +602,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_counter_after_save topic = Topic.create!(title: "monday night") - topic.replies.create!(title: "re: monday night", content: "football") + + assert_queries(2) do + topic.replies.create!(title: "re: monday night", content: "football") + end + assert_equal 1, Topic.find(topic.id)[:replies_count] topic.save! @@ -584,8 +640,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase debate.touch(time: time) debate2.touch(time: time) - reply.parent_title = "debate" - reply.save! + assert_queries(3) do + reply.parent_title = "debate" + reply.save! + end assert_operator debate.reload.updated_at, :>, time assert_operator debate2.reload.updated_at, :>, time @@ -593,7 +651,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase debate.touch(time: time) debate2.touch(time: time) - reply.topic_with_primary_key = debate2 + assert_queries(3) do + reply.topic_with_primary_key = debate2 + reply.save! + end assert_operator debate.reload.updated_at, :>, time assert_operator debate2.reload.updated_at, :>, time @@ -657,7 +718,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase line_item = LineItem.create! Invoice.create!(line_items: [line_item]) - assert_queries(0) { line_item.save } + assert_no_queries { line_item.save } end def test_belongs_to_with_touch_option_on_destroy @@ -752,7 +813,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_dont_find_target_when_foreign_key_is_null tagging = taggings(:thinking_general) - assert_queries(0) { tagging.super_tag } + assert_no_queries { tagging.super_tag } end def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded @@ -772,6 +833,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase reply = Reply.create(title: "re: zoom", content: "speedy quick!") reply.topic = topic + reply.save! assert_equal 1, topic.reload[:replies_count] assert_equal 1, topic.replies.size @@ -827,6 +889,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase silly = SillyReply.create(title: "gaga", content: "boo-boo") silly.reply = reply + silly.save! assert_equal 1, reply.reload[:replies_count] assert_equal 1, reply.replies.size diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index e717621928..a9e22c7643 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -117,9 +117,8 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_has_many_sti_and_subclasses - silly = SillyReply.new(title: "gaga", content: "boo-boo", parent_id: 1) - silly.parent_id = 1 - assert silly.save + reply = Reply.new(title: "gaga", content: "boo-boo", parent_id: 1) + assert reply.save topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a assert_no_queries do @@ -161,6 +160,16 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end end + def test_preload_through_missing_records + post = Post.where.not(author_id: Author.select(:id)).preload(author: { comments: :post }).first! + assert_no_queries { assert_nil post.author } + end + + def test_eager_association_loading_with_missing_first_record + posts = Post.where(id: 3).preload(author: { comments: :post }).to_a + assert_equal posts.size, 1 + end + def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first } diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index c5b2b77bd4..525ad3197a 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -92,7 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase def test_include_query res = ShapeExpression.all.merge!(includes: [ :shape, { paint: :non_poly } ]).to_a assert_equal NUM_SHAPE_EXPRESSIONS, res.size - assert_queries(0) do + assert_no_queries do res.each do |se| assert_not_nil se.paint.non_poly, "this is the association that was loading incorrectly before the change" assert_not_nil se.shape, "just making sure other associations still work" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 5b8d4722af..b37e59038e 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -18,6 +18,7 @@ require "models/job" require "models/subscriber" require "models/subscription" require "models/book" +require "models/citation" require "models/developer" require "models/computer" require "models/project" @@ -29,6 +30,18 @@ require "models/sponsor" require "models/mentor" require "models/contract" +class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase + fixtures :citations + + def test_preloading_too_many_ids + assert_equal Citation.count, Citation.preload(:reference_of).to_a.size + end + + def test_eager_loading_too_may_ids + assert_equal Citation.count, Citation.eager_load(:citations).offset(0).size + end +end + class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, @@ -1333,7 +1346,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_joins_with_includes_should_preload_via_joins post = assert_queries(1) { Post.includes(:comments).joins(:comments).order("posts.id desc").to_a.first } - assert_queries(0) do + assert_no_queries do assert_not_equal 0, post.comments.to_a.count end end @@ -1571,8 +1584,9 @@ class EagerAssociationTest < ActiveRecord::TestCase # CollectionProxy#reader is expensive, so the preloader avoids calling it. test "preloading has_many_through association avoids calling association.reader" do - ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never - Author.preload(:readonly_comments).first! + assert_not_called_on_instance_of(ActiveRecord::Associations::HasManyAssociation, :reader) do + Author.preload(:readonly_comments).first! + end end test "preloading through a polymorphic association doesn't require the association to exist" do diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 5eacb5a3d8..aef8f31112 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -89,6 +89,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) {} + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index f414fbf64b..515eb65d37 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -25,6 +25,8 @@ require "models/user" require "models/member" require "models/membership" require "models/sponsor" +require "models/lesson" +require "models/student" require "models/country" require "models/treaty" require "models/vertex" @@ -275,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_habtm_saving_multiple_relationships new_project = Project.new("name" => "Grimetime") amount_of_developers = 4 - developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse + developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") } new_project.developer_ids = [developers[0].id, developers[1].id] new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] @@ -310,7 +312,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) - proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } + + # Load schema information so we don't query below if running just this test. + Project.define_attribute_methods + + proj = assert_no_queries { devel.projects.build("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj @@ -325,7 +331,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) - proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") } + + # Load schema information so we don't query below if running just this test. + Project.define_attribute_methods + + proj = assert_no_queries { devel.projects.new("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj @@ -546,7 +556,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_predicate project.developers, :loaded? assert_includes project.developers, developer end @@ -741,7 +751,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations developer = developers(:david) developer.projects.reload - assert_queries(0) do + assert_no_queries do developer.project_ids developer.project_ids end @@ -772,6 +782,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort end + def test_singular_ids_are_reloaded_after_collection_concat + student = Student.create(name: "Alberto Almagro") + student.lesson_ids + + lesson = Lesson.create(name: "DSI") + student.lessons << lesson + + assert_includes student.lesson_ids, lesson.id + end + def test_scoped_find_on_through_association_doesnt_return_read_only_records tag = Post.find(1).tags.find_by_name("General") @@ -781,7 +801,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_manys_works - assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set + assert_equal ["$10.00", "$20.00"].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set end def test_symbols_as_keys @@ -859,7 +879,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], projects assert_equal [], projects.where(title: "omg") assert_equal [], projects.pluck(:title) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 0ca902385a..d13e1a86e9 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -27,7 +27,6 @@ require "models/categorization" require "models/minivan" require "models/speedometer" require "models/reference" -require "models/job" require "models/college" require "models/student" require "models/pirate" @@ -114,7 +113,7 @@ end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :author_addresses, :comments, - :posts, :readers, :taggings, :cars, :jobs, :tags, + :posts, :readers, :taggings, :cars, :tags, :categorizations, :zines, :interests def setup @@ -377,6 +376,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal invoice.id, line_item.invoice_id end + class SpecialAuthor < ActiveRecord::Base + self.table_name = "authors" + has_many :books, class_name: "SpecialBook", foreign_key: :author_id + end + + class SpecialBook < ActiveRecord::Base + self.table_name = "books" + + belongs_to :author + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } + end + + def test_association_enum_works_properly + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(read_status: "reading") + author.books << book + + assert_equal "reading", book.read_status + assert_not_equal 0, SpecialAuthor.joins(:books).where(books: { read_status: "reading" }).count + end + # When creating objects on the association, we must not do it within a scope (even though it # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope @@ -438,7 +458,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_finder_method_with_dirty_target company = companies(:first_firm) new_clients = [] - assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") new_clients << company.clients_of_firm.build(name: "Another Client III") @@ -458,7 +482,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_finder_bang_method_with_dirty_target company = companies(:first_firm) new_clients = [] - assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") new_clients << company.clients_of_firm.build(name: "Another Client III") @@ -801,6 +829,48 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object" end + def test_reload_with_query_cache + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + firm = Firm.first + # Populate the cache with a second query + firm.clients.load + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the clients again, populating the cache with a query + assert_queries(1) { firm.clients.reload } + # This query is cached, so it shouldn't make a real SQL query + assert_queries(0) { firm.clients.load } + + assert_equal 1, connection.query_cache.size + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + + def test_reloading_unloaded_associations_with_query_cache + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + firm = Firm.create!(name: "firm name") + client = firm.clients.create!(name: "client name") + firm.clients.to_a # add request to cache + + connection.uncached do + client.update!(name: "new client name") + end + + firm = Firm.find(firm.id) + + assert_equal [client.name], firm.clients.reload.map(&:name) + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_find_all_with_include_and_conditions assert_nothing_raised do Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a @@ -935,8 +1005,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_adding_to_new_record - assert_no_queries(ignore_none: false) do - firm = Firm.new + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new + assert_no_queries do firm.clients_of_firm.concat(Client.new("name" => "Natural Company")) end end @@ -950,7 +1023,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -960,7 +1037,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1017,7 +1098,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) - new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_clients = assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } assert_equal 2, new_clients.size end @@ -1029,10 +1114,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_without_loading_association first_topic = topics(:first) - Reply.column_names assert_equal 1, first_topic.replies.length + # Load schema information so we don't query below if running just this test. + Reply.define_attribute_methods + assert_no_queries do first_topic.replies.build(title: "Not saved", content: "Superstars") assert_equal 2, first_topic.replies.size @@ -1043,7 +1130,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1053,7 +1144,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many_via_block company = companies(:first_firm) - new_clients = assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_clients = assert_no_queries do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end @@ -1066,8 +1161,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_without_loading_association first_firm = companies(:first_firm) - Firm.column_names - Client.column_names assert_equal 2, first_firm.clients_of_firm.size first_firm.clients_of_firm.reset @@ -1177,6 +1270,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, topic.reload.replies.size end + def test_counter_cache_updates_in_memory_after_update_with_inverse_of_disabled + topic = Topic.create!(title: "Zoom-zoom-zoom") + + assert_equal 0, topic.replies_count + + reply1 = Reply.create!(title: "re: zoom", content: "speedy quick!") + reply2 = Reply.create!(title: "re: zoom 2", content: "OMG lol!") + + assert_queries(4) do + topic.replies << [reply1, reply2] + end + + assert_equal 2, topic.replies_count + assert_equal 2, topic.reload.replies_count + end + + def test_counter_cache_updates_in_memory_after_update_with_inverse_of_enabled + category = Category.create!(name: "Counter Cache") + + assert_nil category.categorizations_count + + categorization1 = Categorization.create! + categorization2 = Categorization.create! + + assert_queries(4) do + category.categorizations << [categorization1, categorization2] + end + + assert_equal 2, category.categorizations_count + assert_equal 2, category.reload.categorizations_count + end + def test_pushing_association_updates_counter_cache topic = Topic.order("id ASC").first reply = Reply.create! @@ -1214,7 +1339,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_empty_with_counter_cache post = posts(:welcome) - assert_queries(0) do + assert_no_queries do assert_not_empty post.comments end end @@ -1312,8 +1437,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transaction_when_deleting_new_record - assert_no_queries(ignore_none: false) do - firm = Firm.new + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new + assert_no_queries do client = Client.new("name" => "New Client") firm.clients_of_firm << client firm.clients_of_firm.destroy(client) @@ -1748,7 +1876,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients = [] firm.save - assert_queries(0, ignore_none: true) do + assert_no_queries do firm.clients = [] end @@ -1770,8 +1898,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_replacing_on_new_record - assert_no_queries(ignore_none: false) do - firm = Firm.new + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new + assert_no_queries do firm.clients_of_firm = [Client.new("name" => "New Client")] end end @@ -1783,7 +1914,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations company = companies(:first_firm) company.clients.reload - assert_queries(0) do + assert_no_queries do company.client_ids company.client_ids end @@ -1810,11 +1941,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids_for_association_on_new_record_does_not_try_to_find_records - Company.columns # Load schema information so we don't query below - Contract.columns # if running just this test. + # Load schema information so we don't query below if running just this test. + companies(:first_client).contract_ids company = Company.new - assert_queries(0) do + assert_no_queries do company.contract_ids end @@ -1920,7 +2051,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.load_target assert_predicate firm.clients, :loaded? - assert_no_queries(ignore_none: false) do + assert_no_queries do firm.clients.first assert_equal 2, firm.clients.first(2).size firm.clients.last @@ -2134,21 +2265,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class - ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval(<<-EOF, __FILE__, __LINE__ + 1) - class DeleteAllModel < ActiveRecord::Base - has_many :nonentities, :dependent => :delete_all - end - EOF + assert_not_called_on_instance_of( + ActiveRecord::Reflection::AssociationReflection, + :class_name, + ) do + class_eval(<<-EOF, __FILE__, __LINE__ + 1) + class DeleteAllModel < ActiveRecord::Base + has_many :nonentities, :dependent => :delete_all + end + EOF + end end def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class - ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval(<<-EOF, __FILE__, __LINE__ + 1) - class NullifyModel < ActiveRecord::Base - has_many :nonentities, :dependent => :nullify - end - EOF + assert_not_called_on_instance_of( + ActiveRecord::Reflection::AssociationReflection, + :class_name, + ) do + class_eval(<<-EOF, __FILE__, __LINE__ + 1) + class NullifyModel < ActiveRecord::Base + has_many :nonentities, :dependent => :nullify + end + EOF + end end def test_attributes_are_being_set_when_initialized_from_has_many_association_with_where_clause @@ -2294,6 +2433,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_collection_association_with_private_kernel_method firm = companies(:first_firm) assert_equal [accounts(:signals37)], firm.accounts.open + assert_equal [accounts(:signals37)], firm.accounts.available + end + + def test_association_with_or_doesnt_set_inverse_instance_key + firm = companies(:first_firm) + accounts = firm.accounts.or(Account.where(firm_id: nil)).order(:id) + assert_equal [firm.id, nil], accounts.map(&:firm_id) + end + + def test_association_with_rewhere_doesnt_set_inverse_instance_key + firm = companies(:first_firm) + accounts = firm.accounts.rewhere(firm_id: [firm.id, nil]).order(:id) + assert_equal [firm.id, nil], accounts.map(&:firm_id) end test "first_or_initialize adds the record to the association" do @@ -2325,7 +2477,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "has many associations on new records use null relations" do post = Post.new - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], post.comments assert_equal [], post.comments.where(body: "omg") assert_equal [], post.comments.pluck(:body) diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index d5573b6d02..7b405c74c4 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -71,6 +71,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club1.members.sort_by(&:id) end + def test_preload_multiple_instances_of_the_same_record + club = Club.create!(name: "Aaron cool banana club") + Membership.create! club: club, member: Member.create!(name: "Aaron") + Membership.create! club: club, member: Member.create!(name: "Bob") + + preloaded_clubs = Club.joins(:memberships).preload(:membership).to_a + assert_no_queries { preloaded_clubs.each(&:membership) } + end + def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; "Person"; end @@ -265,7 +274,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_queries(1) { posts(:thinking) } new_person = nil # so block binding catches it - assert_queries(0) do + # Load schema information so we don't query below if running just this test. + Person.define_attribute_methods + + assert_no_queries do new_person = Person.new first_name: "bob" end @@ -285,7 +297,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_associate_new_by_building assert_queries(1) { posts(:thinking) } - assert_queries(0) do + # Load schema information so we don't query below if running just this test. + Person.define_attribute_methods + + assert_no_queries do posts(:thinking).people.build(first_name: "Bob") posts(:thinking).people.new(first_name: "Ted") end @@ -562,10 +577,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:welcome).people = [people(:david)] end - assert_queries(0) { + assert_no_queries do assert_includes posts(:welcome).people, people(:david) assert_not_includes posts(:welcome).people, people(:michael) - } + end assert_includes posts(:welcome).reload.people.reload, people(:david) assert_not_includes posts(:welcome).reload.people.reload, people(:michael) @@ -689,7 +704,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:welcome).people.clear end - assert_queries(0) do + assert_no_queries do assert_empty posts(:welcome).people end @@ -779,7 +794,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations person = people(:michael) person.posts.reload - assert_queries(0) do + assert_no_queries do person.post_ids person.post_ids end @@ -1189,7 +1204,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_associations_on_new_records_use_null_relations person = Person.new - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], person.posts assert_equal [], person.posts.where(body: "omg") assert_equal [], person.posts.pluck(:body) diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index d7e898a1c0..adfb3ce072 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -37,10 +37,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_has_one_cache_nils firm = companies(:another_firm) assert_queries(1) { assert_nil firm.account } - assert_queries(0) { assert_nil firm.account } + assert_no_queries { assert_nil firm.account } - firms = Firm.all.merge!(includes: :account).to_a - assert_queries(0) { firms.each(&:account) } + firms = Firm.includes(:account).to_a + assert_no_queries { firms.each(&:account) } end def test_with_select @@ -231,9 +231,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_build_association_dont_create_transaction - assert_no_queries(ignore_none: false) { - Firm.new.build_account - } + # Load schema information so we don't query below if running just this test. + Account.define_attribute_methods + + firm = Firm.new + assert_no_queries do + firm.build_account + end end def test_building_the_associated_object_with_implicit_sti_base_class @@ -329,6 +333,29 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal 80, odegy.reload_account.credit_limit end + def test_reload_association_with_query_cache + odegy_id = companies(:odegy).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy = Company.find(odegy_id) + # Populate the cache with a second query + odegy.account + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the account again, populating the cache with a query + assert_queries(1) { odegy.reload_account } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Company.find(odegy_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -661,6 +688,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" + + enum status: [:proposed, :written, :published] end class SpecialAuthor < ActiveRecord::Base @@ -678,6 +707,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book + assert_equal "published", book.status assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index da3a42e2b5..eb4dc73423 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -20,6 +20,8 @@ require "models/company" require "models/project" require "models/author" require "models/post" +require "models/department" +require "models/hotel" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -724,6 +726,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end + + def test_favors_has_one_associations_for_inverse_of + inverse_name = Post.reflect_on_association(:author).inverse_of.name + assert_equal :post, inverse_name + end + + def test_finds_inverse_of_for_plural_associations + inverse_name = Department.reflect_on_association(:hotel).inverse_of.name + assert_equal :departments, inverse_name + end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 03ed1c1d47..5821744530 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -137,7 +137,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_one_through_with_has_one_source_reflection_preload members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } mustache = sponsors(:moustache_club_sponsor_for_groucho) - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [mustache], members.first.nested_sponsors end end @@ -196,7 +196,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence # the need to ignore certain predefined sqls that deal with system calls. - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id) end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 739eb02e0c..081da95df7 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -80,7 +80,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload firm = Firm.new("name" => "A New Firm, Inc") firm.save - firm.clients.each {} # forcing to load all clients + firm.clients.each { } # forcing to load all clients assert firm.clients.empty?, "New firm shouldn't have client objects" assert_equal 0, firm.clients.size, "New firm should have 0 clients" diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 0bfd46a522..0dbdd56ae6 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -56,6 +56,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content) end + test "attribute_for_inspect with a non-primary key id attribute" do + t = topics(:first).becomes(TitlePrimaryKeyTopic) + t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" + + assert_equal "1", t.attribute_for_inspect(:id) + end + test "attribute_present" do t = Topic.new t.title = "hello there!" @@ -310,6 +317,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "New topic", topic.title end + test "write_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do + topic = Topic.first + assert_raises(ActiveModel::MissingAttributeError) { topic.update_columns(no_column_exists: "Hello!") } + assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") } + end + test "read_attribute" do topic = Topic.new topic.title = "Don't change the topic" diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 3bc56694be..2632aec7ab 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -148,6 +148,20 @@ module ActiveRecord assert_equal 2, klass.new.counter end + test "procs for default values are evaluated even after column_defaults is called" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + assert_equal 1, klass.new.counter + + # column_defaults will increment the counter since the proc is called + klass.column_defaults + + assert_equal 3, klass.new.counter + end + test "procs are memoized before type casting" do klass = Class.new(OverloadedType) do @@counter = 0 diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index ade1f4b44d..88df0eed55 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -14,6 +14,7 @@ require "models/line_item" require "models/order" require "models/parrot" require "models/pirate" +require "models/project" require "models/ship" require "models/ship_part" require "models/tag" @@ -558,6 +559,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_clients + 1, Client.count end + def test_parent_should_save_children_record_with_foreign_key_validation_set_in_before_save_callback + company = NewlyContractedCompany.new(name: "test") + + assert company.save + assert_not_empty company.reload.new_contracts + end + def test_parent_should_not_get_saved_with_duplicate_children_records assert_no_difference "Reply.count" do assert_no_difference "SillyUniqueReply.count" do @@ -568,7 +576,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa ]) assert_not reply.save - assert_not_empty reply.errors + assert_equal ["is invalid"], reply.errors[:silly_unique_replies] + assert_empty reply.silly_unique_replies.first.errors + + assert_equal( + ["has already been taken"], + reply.silly_unique_replies.last.errors[:content] + ) end end end @@ -628,7 +642,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_before_save company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" @@ -639,7 +657,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_before_save company = companies(:first_firm) - assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } company.name += "-changed" assert_queries(3) { assert company.save } @@ -648,7 +670,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" @@ -659,7 +685,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_via_block_before_save company = companies(:first_firm) - assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end @@ -1086,7 +1116,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase assert @pirate.save Pirate.transaction do - assert_queries(0) do + assert_no_queries do assert @pirate.save end end @@ -1167,12 +1197,12 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_changed_for_autosave_should_handle_cycles @ship.pirate = @pirate - assert_queries(0) { @ship.save! } + assert_no_queries { @ship.save! } @parrot = @pirate.parrots.create(name: "some_name") @parrot.name = "changed_name" assert_queries(1) { @ship.save! } - assert_queries(0) { @ship.save! } + assert_no_queries { @ship.save! } end def test_should_automatically_save_bang_the_associated_model @@ -1774,3 +1804,21 @@ class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::Te assert_equal 1, comment.post_comments_count end end + +class TestAutosaveAssociationOnAHasManyAssociationDefinedInSubclassWithAcceptsNestedAttributes < ActiveRecord::TestCase + def test_should_update_children_when_asssociation_redefined_in_subclass + agency = Agency.create!(name: "Agency") + valid_project = Project.create!(firm: agency, name: "Initial") + agency.update!( + "projects_attributes" => { + "0" => { + "name" => "Updated", + "id" => valid_project.id + } + } + ) + valid_project.reload + + assert_equal "Updated", valid_project.name + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d216fe16fa..09e5517449 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -282,11 +282,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_initialize_with_invalid_attribute - Topic.new("title" => "test", - "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31") - rescue ActiveRecord::MultiparameterAssignmentErrors => ex + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + Topic.new("title" => "test", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00") + end + assert_equal(1, ex.errors.size) - assert_equal("last_read", ex.errors[0].attribute) + assert_equal("written_on", ex.errors[0].attribute) end def test_create_after_initialize_without_block @@ -853,8 +855,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal company, Company.find(company.id) end - # TODO: extend defaults tests to other databases! - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter) def test_default with_timezone_config default: :local do default = Default.new @@ -866,7 +867,10 @@ class BasicsTest < ActiveRecord::TestCase # char types assert_equal "Y", default.char1 assert_equal "a varchar field", default.char2 - assert_equal "a text field", default.char3 + # Mysql text type can't have default value + unless current_adapter?(:Mysql2Adapter) + assert_equal "a text field", default.char3 + end end end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index c8163901c6..d21218a997 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -24,7 +24,7 @@ class EachTest < ActiveRecord::TestCase def test_each_should_not_return_query_chain_and_execute_only_one_query assert_queries(1) do - result = Post.find_each(batch_size: 100000) {} + result = Post.find_each(batch_size: 100000) { } assert_nil result end end @@ -155,7 +155,7 @@ class EachTest < ActiveRecord::TestCase end def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified - not_a_post = "not a post".dup + not_a_post = +"not a post" def not_a_post.id; end not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do assert_nothing_raised do @@ -183,7 +183,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_error_on_ignore_the_order assert_raise(ArgumentError) do - PostWithDefaultScope.find_in_batches(error_on_ignore: true) {} + PostWithDefaultScope.find_in_batches(error_on_ignore: true) { } end end @@ -192,7 +192,7 @@ class EachTest < ActiveRecord::TestCase prev = ActiveRecord::Base.error_on_ignored_order ActiveRecord::Base.error_on_ignored_order = true assert_nothing_raised do - PostWithDefaultScope.find_in_batches(error_on_ignore: false) {} + PostWithDefaultScope.find_in_batches(error_on_ignore: false) { } end ensure # Set back to default @@ -204,7 +204,7 @@ class EachTest < ActiveRecord::TestCase prev = ActiveRecord::Base.error_on_ignored_order ActiveRecord::Base.error_on_ignored_order = true assert_raise(ArgumentError) do - PostWithDefaultScope.find_in_batches() {} + PostWithDefaultScope.find_in_batches() { } end ensure # Set back to default @@ -213,7 +213,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_error_by_default assert_nothing_raised do - PostWithDefaultScope.find_in_batches() {} + PostWithDefaultScope.find_in_batches() { } end end @@ -228,7 +228,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) {} + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) { } end end @@ -420,7 +420,7 @@ class EachTest < ActiveRecord::TestCase end def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified - not_a_post = "not a post".dup + not_a_post = +"not a post" def not_a_post.id raise StandardError.new("not_a_post had #id called on it") end @@ -446,7 +446,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.in_batches({ of: 42, start: 1 }.freeze) {} + Post.in_batches({ of: 42, start: 1 }.freeze) { } end end @@ -597,15 +597,15 @@ class EachTest < ActiveRecord::TestCase table: table_alias, predicate_builder: predicate_builder ) - posts.find_each {} + posts.find_each { } end end test ".find_each bypasses the query cache for its own queries" do Post.cache do assert_queries(2) do - Post.find_each {} - Post.find_each {} + Post.find_each { } + Post.find_each { } end end end @@ -624,8 +624,8 @@ class EachTest < ActiveRecord::TestCase test ".find_in_batches bypasses the query cache for its own queries" do Post.cache do assert_queries(2) do - Post.find_in_batches {} - Post.find_in_batches {} + Post.find_in_batches { } + Post.find_in_batches { } end end end @@ -644,8 +644,8 @@ class EachTest < ActiveRecord::TestCase test ".in_batches bypasses the query cache for its own queries" do Post.cache do assert_queries(2) do - Post.in_batches {} - Post.in_batches {} + Post.in_batches { } + Post.in_batches { } end end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index d5376ece69..58abdb47f7 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -12,7 +12,7 @@ unless current_adapter?(:DB2Adapter) FIXTURES = %w(flowers.jpg example.log test.txt) def test_mixed_encoding - str = "\x80".dup + str = +"\x80" str.force_encoding("ASCII-8BIT") binary = Binary.new name: "いただきます!", data: str diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 91cc49385c..bd5f157ca1 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -34,6 +34,16 @@ if ActiveRecord::Base.connection.prepared_statements ActiveSupport::Notifications.unsubscribe(@subscription) end + def test_too_many_binds + bind_params_length = @connection.send(:bind_params_length) + + topics = Topic.where(id: (1 .. bind_params_length).to_a << 2**63) + assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1 .. bind_params_length).to_a << 2**63) + assert_equal 0, topics.count + end + def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: "David") scope = Author.from(subquery, "authors").where(id: 1) diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 253c3099d6..0ea3fb86a6 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -480,7 +480,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_save_doesnt_allow_on_option exception = assert_raises ArgumentError do Class.new(ActiveRecord::Base) do - before_save(on: :create) {} + before_save(on: :create) { } end end assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message @@ -489,7 +489,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_around_save_doesnt_allow_on_option exception = assert_raises ArgumentError do Class.new(ActiveRecord::Base) do - around_save(on: :create) {} + around_save(on: :create) { } end end assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message @@ -498,7 +498,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_after_save_doesnt_allow_on_option exception = assert_raises ArgumentError do Class.new(ActiveRecord::Base) do - after_save(on: :create) {} + after_save(on: :create) { } end end assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index a5d908344a..483383257b 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -42,6 +42,20 @@ module ActiveRecord assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end + test "cache_key for relation with custom select and limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + developers_with_select = developers.select("developers.*") + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key + + assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + test "cache_key for loaded relation" do developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load last_developer_timestamp = developers.first.updated_at @@ -91,12 +105,12 @@ module ActiveRecord developers = Developer.where(name: "David") assert_queries(1) { developers.cache_key } - assert_queries(0) { developers.cache_key } + assert_no_queries { developers.cache_key } end test "it doesn't trigger any query if the relation is already loaded" do developers = Developer.where(name: "David").load - assert_queries(0) { developers.cache_key } + assert_no_queries { developers.cache_key } end test "relation cache_key changes when the sql query changes" do diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b8e623f17b..51d0cc3d12 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -28,13 +28,16 @@ module ActiveRecord end def test_establish_connection_uses_spec_name + old_config = ActiveRecord::Base.configurations config = { "readonly" => { "adapter" => "sqlite3" } } - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) + ActiveRecord::Base.configurations = config + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations) spec = resolver.spec(:readonly) @handler.establish_connection(spec.to_hash) assert_not_nil @handler.retrieve_connection_pool("readonly") ensure + ActiveRecord::Base.configurations = old_config @handler.remove_connection("readonly") end @@ -148,6 +151,35 @@ module ActiveRecord ActiveRecord::Base.configurations = @prev_configs end + def test_symbolized_configurations_assignment + @prev_configs = ActiveRecord::Base.configurations + config = { + development: { + primary: { + adapter: "sqlite3", + database: "db/development.sqlite3", + }, + }, + test: { + primary: { + adapter: "sqlite3", + database: "db/test.sqlite3", + }, + }, + } + ActiveRecord::Base.configurations = config + ActiveRecord::Base.configurations.configs_for.each do |db_config| + assert_instance_of ActiveRecord::DatabaseConfigurations::HashConfig, db_config + assert_instance_of String, db_config.env_name + assert_instance_of String, db_config.spec_name + db_config.config.keys.each do |key| + assert_instance_of String, key + end + end + ensure + ActiveRecord::Base.configurations = @prev_configs + end + def test_retrieve_connection assert @handler.retrieve_connection(@spec_name) end diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb new file mode 100644 index 0000000000..a520fb1303 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/person" + +module ActiveRecord + module ConnectionAdapters + class ConnectionHandlersMultiDbTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :people + + def setup + @handlers = { writing: ConnectionHandler.new, reading: ConnectionHandler.new } + @rw_handler = @handlers[:writing] + @ro_handler = @handlers[:reading] + @spec_name = "primary" + @rw_pool = @handlers[:writing].establish_connection(ActiveRecord::Base.configurations["arunit"]) + @ro_pool = @handlers[:reading].establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + def teardown + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + + class MultiConnectionTestModel < ActiveRecord::Base + end + + def test_multiple_connection_handlers_works_in_a_threaded_environment + tf_writing = Tempfile.open "test_writing" + tf_reading = Tempfile.open "test_reading" + + MultiConnectionTestModel.connects_to database: { writing: { database: tf_writing.path, adapter: "sqlite3" }, reading: { database: tf_reading.path, adapter: "sqlite3" } } + + MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))") + MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('writing')") + + ActiveRecord::Base.connected_to(role: :reading) do + MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))") + MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('reading')") + end + + read_latch = Concurrent::CountDownLatch.new + write_latch = Concurrent::CountDownLatch.new + + MultiConnectionTestModel.connection + + thread = Thread.new do + MultiConnectionTestModel.connection + + write_latch.wait + assert_equal "writing", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1") + read_latch.count_down + end + + ActiveRecord::Base.connected_to(role: :reading) do + write_latch.count_down + assert_equal "reading", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1") + read_latch.wait + end + + thread.join + ensure + tf_reading.close + tf_reading.unlink + tf_writing.close + tf_writing.unlink + end + + unless in_memory_db? + def test_establish_connection_using_3_levels_config + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly }) + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_via_handler + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly }) + + ActiveRecord::Base.connected_to(role: :reading) do + @ro_handler = ActiveRecord::Base.connection_handler + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading] + end + + ActiveRecord::Base.connected_to(role: :writing) do + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_with_database_url + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo" + + ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + ENV["DATABASE_URL"] = previous_url + end + + def test_switching_connections_with_database_config_hash + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + config = { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + + ActiveRecord::Base.connected_to(database: { writing: config }) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_with_database_symbol + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + ActiveRecord::Base.connected_to(database: :arunit2) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:arunit2] + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_connects_to_with_single_configuration + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to database: { writing: :development } + + assert_equal 1, ActiveRecord::Base.connection_handlers.size + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end + + def test_connects_to_using_top_level_key_in_two_level_config + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly } + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end + end + + def test_connection_pools + assert_equal([@rw_pool], @handlers[:writing].connection_pools) + assert_equal([@ro_pool], @handlers[:reading].connection_pools) + end + + def test_retrieve_connection + assert @rw_handler.retrieve_connection(@spec_name) + assert @ro_handler.retrieve_connection(@spec_name) + end + + def test_active_connections? + assert_not_predicate @rw_handler, :active_connections? + assert_not_predicate @ro_handler, :active_connections? + + assert @rw_handler.retrieve_connection(@spec_name) + assert @ro_handler.retrieve_connection(@spec_name) + + assert_predicate @rw_handler, :active_connections? + assert_predicate @ro_handler, :active_connections? + + @rw_handler.clear_active_connections! + assert_not_predicate @rw_handler, :active_connections? + + @ro_handler.clear_active_connections! + assert_not_predicate @ro_handler, :active_connections? + end + + def test_retrieve_connection_pool + assert_not_nil @rw_handler.retrieve_connection_pool(@spec_name) + assert_not_nil @ro_handler.retrieve_connection_pool(@spec_name) + end + + def test_retrieve_connection_pool_with_invalid_id + assert_nil @rw_handler.retrieve_connection_pool("foo") + assert_nil @ro_handler.retrieve_connection_pool("foo") + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 1b64324cc4..06c1c51724 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -18,11 +18,14 @@ module ActiveRecord end def resolve_config(config) - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + configs = ActiveRecord::DatabaseConfigurations.new(config) + configs.to_h end def resolve_spec(spec, config) - ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def test_resolver_with_database_uri_and_current_env_symbol_key diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 0941ee3309..b9b5cc0e28 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -106,7 +106,7 @@ module ActiveRecord def middleware(app) lambda do |env| a, b, c = executor.wrap { app.call(env) } - [a, b, Rack::BodyProxy.new(c) {}] + [a, b, Rack::BodyProxy.new(c) { }] end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 9ac03629c3..633d56e479 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -91,7 +91,9 @@ module ActiveRecord end def test_full_pool_exception + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout @pool.size.times { assert @pool.checkout } + assert_raises(ConnectionTimeoutError) do @pool.checkout end @@ -109,6 +111,44 @@ module ActiveRecord assert_equal connection, t.join.value end + def test_full_pool_blocking_shares_load_interlock + @pool.instance_variable_set(:@size, 1) + + load_interlock_latch = Concurrent::CountDownLatch.new + connection_latch = Concurrent::CountDownLatch.new + + able_to_get_connection = false + able_to_load = false + + thread_with_load_interlock = Thread.new do + ActiveSupport::Dependencies.interlock.running do + load_interlock_latch.count_down + connection_latch.wait + + @pool.with_connection do + able_to_get_connection = true + end + end + end + + thread_with_last_connection = Thread.new do + @pool.with_connection do + connection_latch.count_down + load_interlock_latch.wait + + ActiveSupport::Dependencies.interlock.loading do + able_to_load = true + end + end + end + + thread_with_load_interlock.join + thread_with_last_connection.join + + assert able_to_get_connection + assert able_to_load + end + def test_removing_releases_latch cs = @pool.size.times.map { @pool.checkout } t = Thread.new { @pool.checkout } @@ -156,6 +196,48 @@ module ActiveRecord @pool.connections.each { |conn| conn.close if conn.in_use? } end + def test_idle_timeout_configuration + @pool.disconnect! + spec = ActiveRecord::Base.connection_pool.spec + spec.config.merge!(idle_timeout: "0.02") + @pool = ConnectionPool.new(spec) + idle_conn = @pool.checkout + @pool.checkin(idle_conn) + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 0.01 + ) + + @pool.flush + assert_equal 1, @pool.connections.length + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 0.02 + ) + + @pool.flush + assert_equal 0, @pool.connections.length + end + + def test_disable_flush + @pool.disconnect! + spec = ActiveRecord::Base.connection_pool.spec + spec.config.merge!(idle_timeout: -5) + @pool = ConnectionPool.new(spec) + idle_conn = @pool.checkout + @pool.checkin(idle_conn) + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 1 + ) + + @pool.flush + assert_equal 1, @pool.connections.length + end + def test_flush idle_conn = @pool.checkout recent_conn = @pool.checkout @@ -166,9 +248,10 @@ module ActiveRecord assert_equal 3, @pool.connections.length - def idle_conn.seconds_idle - 1000 - end + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 1000 + ) @pool.flush(30) @@ -578,7 +661,7 @@ module ActiveRecord end stuck_thread = Thread.new do - pool.with_connection {} + pool.with_connection { } end # wait for stuck_thread to get in queue diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 5b80f16a44..72be14f507 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -7,11 +7,15 @@ module ActiveRecord class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase def resolve(spec, config = {}) - Resolver.new(config).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def spec(spec, config = {}) - Resolver.new(config).spec(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.spec(spec) end def test_url_invalid_adapter diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 6e7ae2efb4..36e3d543cd 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -30,13 +30,18 @@ class CoreTest < ActiveRecord::TestCase assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect end + def test_inspect_instance_with_non_primary_key_id_attribute + topic = topics(:first).becomes(TitlePrimaryKeyTopic) + assert_match(/id: 1/, topic.inspect) + end + def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end def test_pretty_print_new topic = Topic.new - actual = "".dup + actual = +"" PP.pp(topic, StringIO.new(actual)) expected = <<~PRETTY #<Topic:0xXXXXXX @@ -65,7 +70,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_persisted topic = topics(:first) - actual = "".dup + actual = +"" PP.pp(topic, StringIO.new(actual)) expected = <<~PRETTY #<Topic:0x\\w+ @@ -93,7 +98,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_uninitialized topic = Topic.allocate - actual = "".dup + actual = +"" PP.pp(topic, StringIO.new(actual)) expected = "#<Topic:XXXXXX not initialized>\n" assert actual.start_with?(expected.split("XXXXXX").first) @@ -106,8 +111,15 @@ class CoreTest < ActiveRecord::TestCase "inspecting topic" end end - actual = "".dup + actual = +"" PP.pp(subtopic.new, StringIO.new(actual)) assert_equal "inspecting topic\n", actual end + + def test_pretty_print_with_non_primary_key_id_attribute + topic = topics(:first).becomes(TitlePrimaryKeyTopic) + actual = +"" + PP.pp(topic, StringIO.new(actual)) + assert_match(/id: 1/, actual) + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 0f957d41cf..5d02e59ef6 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -106,6 +106,13 @@ if current_adapter?(:Mysql2Adapter) class MysqlDefaultExpressionTest < ActiveRecord::TestCase include SchemaDumpingHelper + if supports_default_expression? + test "schema dump includes default expression" do + output = dump_table_schema("defaults") + assert_match %r/t\.binary\s+"uuid",\s+limit: 36,\s+default: -> { "\(uuid\(\)\)" }/i, output + end + end + if subsecond_precision_supported? test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 83cc2aa319..dfd74bfcb4 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -336,7 +336,7 @@ class DirtyTest < ActiveRecord::TestCase end with_partial_writes Pirate, true do - assert_queries(0) { 2.times { pirate.save! } } + assert_no_queries { 2.times { pirate.save! } } assert_equal old_updated_on, pirate.reload.updated_on assert_queries(1) { pirate.catchphrase = "bar"; pirate.save! } @@ -355,7 +355,7 @@ class DirtyTest < ActiveRecord::TestCase old_lock_version = person.lock_version with_partial_writes Person, true do - assert_queries(0) { 2.times { person.save! } } + assert_no_queries { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version assert_queries(1) { person.first_name = "bar"; person.save! } @@ -567,8 +567,6 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_not pirate.previous_changes.key?("parrot_id") assert_not pirate.previous_changes.key?("created_on") - ensure - travel_back end class Testings < ActiveRecord::Base; end @@ -879,6 +877,26 @@ class DirtyTest < ActiveRecord::TestCase raise "changed? should be false" if changed? raise "has_changes_to_save? should be false" if has_changes_to_save? raise "saved_changes? should be true" unless saved_changes? + raise "id_in_database should not be nil" if id_in_database.nil? + end + end + + person = klass.create!(first_name: "Sean") + assert_not_predicate person, :changed? + end + + test "changed? in around callbacks after yield returns false" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "people" + + around_create :check_around + + def check_around + yield + raise "changed? should be false" if changed? + raise "has_changes_to_save? should be false" if has_changes_to_save? + raise "saved_changes? should be true" unless saved_changes? + raise "id_in_database should not be nil" if id_in_database.nil? end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index d5a1d11e12..867ce7082b 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -265,6 +265,35 @@ class EnumTest < ActiveRecord::TestCase assert_equal "published", @book.status end + test "invalid definition values raise an ArgumentError" do + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [proposed: 1, written: 2, published: 3] + end + end + + assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: { "" => 1, "active" => 2 } + end + end + + assert_match(/Enum label name must not be blank/, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: ["active", ""] + end + end + + assert_match(/Enum label name must not be blank/, e.message) + end + test "reserved enum names" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 82cc891970..79a0630193 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -40,7 +40,7 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal binds, queries[0][1] end - def test_collects_nothing_if_the_statement_is_not_whitelisted + def test_collects_nothing_if_the_statement_is_not_explainable SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length") assert_empty queries end diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb new file mode 100644 index 0000000000..47161a547a --- /dev/null +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/admin" +require "models/admin/user" +require "models/admin/account" +require "models/user" +require "pp" + +class FilterAttributesTest < ActiveRecord::TestCase + fixtures :"admin/users", :"admin/accounts" + + setup do + @previous_filter_attributes = ActiveRecord::Base.filter_attributes + ActiveRecord::Base.filter_attributes = [:name] + end + + teardown do + ActiveRecord::Base.filter_attributes = @previous_filter_attributes + end + + test "filter_attributes" do + Admin::User.all.each do |user| + assert_includes user.inspect, "name: [FILTERED]" + assert_equal 1, user.inspect.scan("[FILTERED]").length + end + + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + end + + test "string filter_attributes perform pertial match" do + ActiveRecord::Base.filter_attributes = ["n"] + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + end + + test "regex filter_attributes are accepted" do + ActiveRecord::Base.filter_attributes = [/\An\z/] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.inspect, 'name: "37signals"' + assert_equal 0, account.inspect.scan("[FILTERED]").length + + ActiveRecord::Base.filter_attributes = [/\An/] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.reload.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + + test "proc filter_attributes are accepted" do + ActiveRecord::Base.filter_attributes = [ lambda { |key, value| value.reverse! if key == "name" } ] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.inspect, 'name: "slangis73"' + end + + test "filter_attributes could be overwritten by models" do + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + + begin + Admin::Account.filter_attributes = [] + + # Above changes should not impact other models + Admin::User.all.each do |user| + assert_includes user.inspect, "name: [FILTERED]" + assert_equal 1, user.inspect.scan("[FILTERED]").length + end + + Admin::Account.all.each do |account| + assert_not_includes account.inspect, "name: [FILTERED]" + assert_equal 0, account.inspect.scan("[FILTERED]").length + end + ensure + Admin::Account.remove_instance_variable(:@filter_attributes) + end + end + + test "filter_attributes should not filter nil value" do + account = Admin::Account.new + + assert_includes account.inspect, "name: nil" + assert_not_includes account.inspect, "name: [FILTERED]" + assert_equal 0, account.inspect.scan("[FILTERED]").length + end + + test "filter_attributes should handle [FILTERED] value properly" do + begin + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + + assert_includes user.inspect, "auth_token: [FILTERED]" + assert_includes user.inspect, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) + end + end + + test "filter_attributes on pretty_print" do + user = admin_users(:david) + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "name: [FILTERED]" + assert_equal 1, actual.scan("[FILTERED]").length + end + + test "filter_attributes on pretty_print should not filter nil value" do + user = Admin::User.new + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "name: nil" + assert_not_includes actual, "name: [FILTERED]" + assert_equal 0, actual.scan("[FILTERED]").length + end + + test "filter_attributes on pretty_print should handle [FILTERED] value properly" do + begin + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "auth_token: [FILTERED]" + assert_includes actual, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) + end + end +end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 59af4e6961..e0acd30c22 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -12,7 +12,7 @@ class FinderRespondToTest < ActiveRecord::TestCase end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method - class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) {} + class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { } assert_respond_to Topic, :method_added_for_finder_respond_to_test ensure class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test) diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index e73c88dd5d..52fd9291b2 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -20,6 +20,7 @@ require "models/matey" require "models/dog" require "models/car" require "models/tyre" +require "models/subscriber" class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars @@ -167,6 +168,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?(id: [1, 9999]) assert_equal false, Topic.exists?(45) + assert_equal false, Topic.exists?(9999999999999999999999999999999) assert_equal false, Topic.exists?(Topic.new.id) assert_raise(NoMethodError) { Topic.exists?([1, 2]) } @@ -211,17 +213,23 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, relation.exists?(false) end + def test_exists_with_string + assert_equal false, Subscriber.exists?("foo") + assert_equal false, Subscriber.exists?(" ") + + Subscriber.create!(id: "foo") + Subscriber.create!(id: " ") + + assert_equal true, Subscriber.exists?("foo") + assert_equal true, Subscriber.exists?(" ") + end + def test_exists_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.exists?(Topic.new) end end - def test_exists_returns_false_when_parameter_has_invalid_type - assert_equal false, Topic.exists?("foo") - assert_equal false, Topic.exists?(("9" * 53).to_i) # number that's bigger than int - end - def test_exists_does_not_select_columns_without_alias assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do Topic.exists? @@ -246,6 +254,10 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.first.replies.exists? end + def test_exists_with_empty_hash_arg + assert_equal true, Topic.exists?({}) + end + # Ensure +exists?+ runs without an error by excluding distinct value. # See https://github.com/rails/rails/pull/26981. def test_exists_with_order_and_distinct @@ -371,7 +383,10 @@ class FinderTest < ActiveRecord::TestCase end def test_find_an_empty_array - assert_equal [], Topic.find([]) + empty_array = [] + result = Topic.find(empty_array) + assert_equal [], result + assert_not_same empty_array, result end def test_find_doesnt_have_implicit_ordering diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index c65523d8c1..a5592fc86a 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -473,11 +473,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new(nil, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new(nil, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -487,14 +487,14 @@ class FixturesTest < ActiveRecord::TestCase assert_empty Dir[nonexistent_fixture_path + "*"] assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) + ActiveRecord::FixtureSet.new(nil, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file fixture_path = FIXTURES_ROOT + "/naked/yml/courses" error = assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + ActiveRecord::FixtureSet.new(nil, "courses", Course, fixture_path) end assert_equal "fixture is not a hash: #{fixture_path}.yml", error.to_s end @@ -502,7 +502,7 @@ class FixturesTest < ActiveRecord::TestCase def test_yaml_file_with_one_invalid_fixture fixture_path = FIXTURES_ROOT + "/naked/yml/courses_with_invalid_key" error = assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + ActiveRecord::FixtureSet.new(nil, "courses", Course, fixture_path) end assert_equal "fixture key is not a hash: #{fixture_path}.yml, keys: [\"two\"]", error.to_s end @@ -525,7 +525,7 @@ class FixturesTest < ActiveRecord::TestCase def test_omap_fixtures assert_nothing_raised do - fixtures = ActiveRecord::FixtureSet.new(Account.connection, "categories", Category, FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::FixtureSet.new(nil, "categories", Category, FIXTURES_ROOT + "/categories_ordered") fixtures.each.with_index do |(name, fixture), i| assert_equal "fixture_no_#{i}", name @@ -596,7 +596,7 @@ class HasManyThroughFixture < ActiveRecord::TestCase parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) rows = fs.table_rows assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrots_treasures"] end @@ -614,18 +614,22 @@ class HasManyThroughFixture < ActiveRecord::TestCase parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) rows = fs.table_rows assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrot_treasures"] end + def test_has_and_belongs_to_many_order + assert_equal ["parrots", "parrots_treasures"], load_has_and_belongs_to_many.keys + end + def load_has_and_belongs_to_many parrot = make_model "Parrot" parrot.has_and_belongs_to_many :treasures parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) fs.table_rows end end @@ -974,7 +978,7 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase connection_id: connection.object_id } - message_bus.instrument("!connection.active_record", payload) {} + message_bus.instrument("!connection.active_record", payload) { } end end end @@ -1344,3 +1348,19 @@ class SameNameDifferentDatabaseFixturesTest < ActiveRecord::TestCase assert_kind_of OtherDog, other_dogs(:lassie) end end + +class NilFixturePathTest < ActiveRecord::TestCase + test "raises an error when all fixtures loaded" do + error = assert_raises(StandardError) do + TestCase = Class.new(ActiveRecord::TestCase) + TestCase.class_eval do + self.fixture_path = nil + fixtures :all + end + end + assert_equal <<~MSG.squish, error.message + No fixture path found. + Please set `NilFixturePathTest::TestCase.fixture_path`. + MSG + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 66f11fe5bd..730cd663a2 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -48,6 +48,15 @@ def mysql_enforcing_gtid_consistency? current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency") end +def supports_default_expression? + if current_adapter?(:PostgreSQLAdapter) + true + elsif current_adapter?(:Mysql2Adapter) + conn = ActiveRecord::Base.connection + !conn.mariadb? && conn.version >= "8.0.13" + end +end + def supports_savepoints? ActiveRecord::Base.connection.supports_savepoints? end @@ -183,5 +192,3 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end - -require "mocha/minitest" # FIXME: stop using mocha diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index e7778af55b..7b388ebc5e 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -56,7 +56,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase assert_equal "bar", record.foo end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.prepared_statements test "cleans up after prepared statement failure in a transaction" do with_two_connections do |original_connection, ddl_connection| record = @klass.create! bar: "bar" diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 36cd63c4d4..5687afbc71 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -157,18 +157,40 @@ class IntegrationTest < ActiveRecord::TestCase skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first key = dev.cache_key - dev.touch + travel_to dev.updated_at + 0.000001 do + dev.touch + end assert_not_equal key, dev.cache_key end def test_cache_key_format_is_not_too_precise - skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first dev.touch key = dev.cache_key assert_equal key, dev.reload.cache_key end + def test_cache_version_format_is_precise_enough + skip("Subsecond precision is not supported") unless subsecond_precision_supported? + with_cache_versioning do + dev = Developer.first + version = dev.cache_version.to_param + travel_to Developer.first.updated_at + 0.000001 do + dev.touch + end + assert_not_equal version, dev.cache_version.to_param + end + end + + def test_cache_version_format_is_not_too_precise + with_cache_versioning do + dev = Developer.first + dev.touch + key = dev.cache_version.to_param + assert_equal key, dev.reload.cache_version.to_param + end + end + def test_named_timestamps_for_cache_key assert_deprecated do owner = owners(:blackbeard) @@ -185,50 +207,52 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_is_stable_with_versioning_on - Developer.cache_versioning = true - - developer = Developer.first - first_key = developer.cache_key + with_cache_versioning do + developer = Developer.first + first_key = developer.cache_key - developer.touch - second_key = developer.cache_key + developer.touch + second_key = developer.cache_key - assert_equal first_key, second_key - ensure - Developer.cache_versioning = false + assert_equal first_key, second_key + end end def test_cache_version_changes_with_versioning_on - Developer.cache_versioning = true - - developer = Developer.first - first_version = developer.cache_version + with_cache_versioning do + developer = Developer.first + first_version = developer.cache_version - travel 10.seconds do - developer.touch - end + travel 10.seconds do + developer.touch + end - second_version = developer.cache_version + second_version = developer.cache_version - assert_not_equal first_version, second_version - ensure - Developer.cache_versioning = false + assert_not_equal first_version, second_version + end end def test_cache_key_retains_version_when_custom_timestamp_is_used - Developer.cache_versioning = true + with_cache_versioning do + developer = Developer.first + first_key = developer.cache_key_with_version - developer = Developer.first - first_key = developer.cache_key_with_version + travel 10.seconds do + developer.touch + end - travel 10.seconds do - developer.touch - end + second_key = developer.cache_key_with_version - second_key = developer.cache_key_with_version + assert_not_equal first_key, second_key + end + end - assert_not_equal first_key, second_key + def with_cache_versioning(value = true) + @old_cache_versioning = ActiveRecord::Base.cache_versioning + ActiveRecord::Base.cache_versioning = value + yield ensure - Developer.cache_versioning = false + ActiveRecord::Base.cache_versioning = @old_cache_versioning end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 363beb4780..6cf17ac15d 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -22,6 +22,14 @@ module ActiveRecord end end + class InvertibleTransactionMigration < InvertibleMigration + def change + transaction do + super + end + end + end + class InvertibleRevertMigration < SilentMigration def change revert do @@ -271,6 +279,14 @@ module ActiveRecord assert_not revert.connection.table_exists?("horses") end + def test_migrate_revert_transaction + migration = InvertibleTransactionMigration.new + migration.migrate :up + assert migration.connection.table_exists?("horses") + migration.migrate :down + assert_not migration.connection.table_exists?("horses") + end + def test_migrate_revert_change_column_default migration1 = ChangeColumnDefault1.new migration1.migrate(:up) diff --git a/activerecord/test/cases/legacy_configurations_test.rb b/activerecord/test/cases/legacy_configurations_test.rb new file mode 100644 index 0000000000..c36feb5116 --- /dev/null +++ b/activerecord/test/cases/legacy_configurations_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class LegacyConfigurationsTest < ActiveRecord::TestCase + def test_can_turn_configurations_into_a_hash + assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not." + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort + end + + def test_each_is_deprecated + assert_deprecated do + ActiveRecord::Base.configurations.each do |db_config| + assert_equal "primary", db_config.spec_name + end + end + end + + def test_first_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_fetch_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.fetch("arunit").first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_values_are_deprecated + config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config) + assert_deprecated do + assert_equal config_hashes, ActiveRecord::Base.configurations.values + end + end + end +end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index f0126fdb0d..ae2597adc8 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -44,6 +44,7 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger Developer.primary_key + ActiveRecord::Base.connection.materialize_transactions super ActiveRecord::LogSubscriber.attach_to(:active_record) end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 034bf32165..c108d372d1 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -164,6 +164,14 @@ module ActiveRecord end end + def test_column_creates_column_with_index + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + @connection.expect :add_index, nil, [:delete_me, :bar, {}] + t.column :bar, :integer, index: true + end + end + def test_index_creates_index with_change_table do |t| @connection.expect :add_index, nil, [:delete_me, :bar, {}] diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 3a11bb081b..01f8628fc5 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -117,13 +117,13 @@ module ActiveRecord end def test_invert_create_table_with_options_and_block - block = Proc.new {} + block = Proc.new { } drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table end def test_invert_drop_table - block = Proc.new {} + block = Proc.new { } create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block assert_equal [:create_table, [:people_reminders, id: false], block], create_table end @@ -145,7 +145,7 @@ module ActiveRecord end def test_invert_drop_join_table - block = Proc.new {} + block = Proc.new { } create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table end @@ -329,11 +329,24 @@ module ActiveRecord assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable end + def test_invert_remove_foreign_key_with_primary_key_and_to_table_in_options + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, primary_key: "uuid"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "uuid"]], enable + end + def test_invert_remove_foreign_key_with_on_delete_on_update enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable end + def test_invert_remove_foreign_key_with_to_table_in_options + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, column: :owner_id] + assert_equal [:add_foreign_key, [:dogs, :people, column: :owner_id]], enable + end + def test_invert_remove_foreign_key_is_irreversible_without_to_table assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] @@ -347,6 +360,16 @@ module ActiveRecord @recorder.inverse_of :remove_foreign_key, [:dogs] end end + + def test_invert_transaction_with_irreversible_inside_is_irreversible + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.revert do + @recorder.transaction do + @recorder.execute "some sql" + end + end + end + end end end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 69a50674af..017ee7951e 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -127,6 +127,20 @@ module ActiveRecord assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) end + def test_legacy_migrations_not_raise_exception_on_reverting_transaction + migration = Class.new(ActiveRecord::Migration[5.2]) { + def change + transaction do + execute "select 1" + end + end + }.new + + assert_nothing_raised do + migration.migrate(:down) + end + end + if current_adapter?(:PostgreSQLAdapter) class Testing < ActiveRecord::Base end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index c471dd1106..bb233fbf74 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -53,16 +53,49 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? end def test_change_column_of_parent_table - foreign_keys = ActiveRecord::Base.connection.foreign_keys("astronauts") rocket = Rocket.create!(name: "myrocket") rocket.astronauts << Astronaut.create! @connection.change_column_null :rockets, :name, false + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + + def test_rename_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.rename_column :astronauts, :name, :astronaut_name + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + + def test_rename_reference_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.rename_column :astronauts, :rocket_id, :new_rocket_id + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + fk = foreign_keys.first assert_equal "myrocket", Rocket.first.name assert_equal "astronauts", fk.from_table assert_equal "rockets", fk.to_table + assert_equal "new_rocket_id", fk.options[:column] end end end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index dedb5ea502..119bfd372a 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -25,7 +25,7 @@ module ActiveRecord ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_raises ActiveRecord::PendingMigrationError do - CheckPending.new(Proc.new {}).call({}) + CheckPending.new(Proc.new { }).call({}) end end @@ -34,7 +34,7 @@ module ActiveRecord migrator = Base.connection.migration_context capture(:stdout) { migrator.migrate } - assert_nil CheckPending.new(Proc.new {}).call({}) + assert_nil CheckPending.new(Proc.new { }).call({}) end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index d1292dc53d..661163b4a1 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -87,7 +87,6 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up @@ -100,24 +99,18 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.create!(version: 3) assert_equal true, migrator.needs_migration? - ensure - ActiveRecord::MigrationContext.new(old_path) end def test_migration_detection_without_schema_migration_table ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(migrations_path) assert_equal true, migrator.needs_migration? - ensure - ActiveRecord::MigrationContext.new(old_path) end def test_any_migrations - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") assert_predicate migrator, :any_migrations? @@ -125,8 +118,6 @@ class MigrationTest < ActiveRecord::TestCase migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty") assert_not_predicate migrator_empty, :any_migrations? - ensure - ActiveRecord::MigrationContext.new(old_path) end def test_migration_version @@ -136,6 +127,36 @@ class MigrationTest < ActiveRecord::TestCase assert_equal 20131219224947, migrator.current_version end + def test_create_table_raises_if_already_exists + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_raise(ActiveRecord::StatementInvalid) do + connection.create_table :testings do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + + def test_create_table_with_if_not_exists_true + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_nothing_raised do + connection.create_table :testings, if_not_exists: true do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table # using a copy as we need the drop_table method to # continue to work for the ensure block of the test @@ -393,7 +414,6 @@ class MigrationTest < ActiveRecord::TestCase def test_internal_metadata_stores_environment current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up @@ -410,7 +430,6 @@ class MigrationTest < ActiveRecord::TestCase migrator.up assert_equal new_env, ActiveRecord::InternalMetadata[:environment] ensure - migrator = ActiveRecord::MigrationContext.new(old_path) ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env migrator.up @@ -422,16 +441,12 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] - ensure - migrator = ActiveRecord::MigrationContext.new(old_path) - migrator.up end def test_proper_table_name_on_migration @@ -793,12 +808,20 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end def test_adding_multiple_columns - assert_queries(1) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 1, + "PostgreSQLAdapter" => 2, # one for bulk change, one for comment + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.column :name, :string t.string :qualification, :experience t.integer :age, default: 0 - t.date :birthdate + t.date :birthdate, comment: "This is a comment" t.timestamps null: true end end @@ -806,6 +829,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal 8, columns.size [:name, :qualification, :experience].each { |s| assert_equal :string, column(s).type } assert_equal "0", column(:age).default + assert_equal "This is a comment", column(:birthdate).comment end def test_removing_columns @@ -1150,7 +1174,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_check_pending_with_stdlib_logger old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout) quietly do - assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) } + assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new { }).call({}) } end ensure ActiveRecord::Base.logger = old diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 873455cf67..30e199f1c5 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -100,7 +100,6 @@ class MigratorTest < ActiveRecord::TestCase def test_finds_migrations_in_subdirectories migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations - [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first assert_equal migrations[i].name, pair.last diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index aa519fd332..bb1c1ea17d 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -217,6 +217,18 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase mean_pirate.parrot_attributes = { name: "James" } assert_equal "James", mean_pirate.parrot.name end + + def test_should_not_create_duplicates_with_create_with + Man.accepts_nested_attributes_for(:interests) + + assert_difference("Interest.count", 1) do + Man.create_with( + interests_attributes: [{ topic: "Pirate king" }] + ).find_or_create_by!( + name: "Monkey D. Luffy" + ) + end + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb index 17527568f8..ee96ea1af6 100644 --- a/activerecord/test/cases/null_relation_test.rb +++ b/activerecord/test/cases/null_relation_test.rb @@ -10,26 +10,27 @@ class NullRelationTest < ActiveRecord::TestCase fixtures :posts, :comments def test_none - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], Developer.none assert_equal [], Developer.all.none end end def test_none_chainable - assert_no_queries(ignore_none: false) do + Developer.send(:load_schema) + assert_no_queries do assert_equal [], Developer.none.where(name: "David") end end def test_none_chainable_to_existing_scope_extension_method - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal 1, Topic.anonymous_extension.none.one end end def test_none_chained_to_methods_firing_queries_straight_to_db - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], Developer.none.pluck(:id, :name) assert_equal 0, Developer.none.delete_all assert_equal 0, Developer.none.update_all(name: "David") @@ -39,7 +40,7 @@ class NullRelationTest < ActiveRecord::TestCase end def test_null_relation_content_size_methods - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal 0, Developer.none.size assert_equal 0, Developer.none.count assert_equal true, Developer.none.empty? @@ -61,7 +62,7 @@ class NullRelationTest < ActiveRecord::TestCase [:count, :sum].each do |method| define_method "test_null_relation_#{method}" do - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal 0, Comment.none.public_send(method, :id) assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id) end @@ -70,7 +71,7 @@ class NullRelationTest < ActiveRecord::TestCase [:average, :minimum, :maximum].each do |method| define_method "test_null_relation_#{method}" do - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_nil Comment.none.public_send(method, :id) assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id) end diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index 14db63890e..304714979c 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -25,7 +25,6 @@ class NumericDataTest < ActiveRecord::TestCase assert m.save m1 = NumericData.find(m.id) - assert_not_nil m1 assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -50,7 +49,6 @@ class NumericDataTest < ActiveRecord::TestCase assert m.save m1 = NumericData.find(m.id) - assert_not_nil m1 assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -64,4 +62,26 @@ class NumericDataTest < ActiveRecord::TestCase assert_kind_of BigDecimal, m1.big_bank_balance assert_equal BigDecimal("234000567.95"), m1.big_bank_balance end + + if current_adapter?(:PostgreSQLAdapter) + def test_numeric_fields_with_nan + m = NumericData.new( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN"), + world_population: 2**62, + my_house_population: 3 + ) + assert_predicate m.bank_balance, :nan? + assert_predicate m.big_bank_balance, :nan? + assert m.save + + m1 = NumericData.find_by( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN") + ) + + assert_predicate m1.bank_balance, :nan? + assert_predicate m1.big_bank_balance, :nan? + end + end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 7348a22dd3..4830ff2b5f 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -13,90 +13,15 @@ require "models/developer" require "models/computer" require "models/project" require "models/minimalistic" -require "models/warehouse_thing" require "models/parrot" require "models/minivan" -require "models/owner" require "models/person" -require "models/pet" require "models/ship" -require "models/toy" require "models/admin" require "models/admin/user" -require "rexml/document" class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys - - # Oracle UPDATE does not support ORDER BY - unless current_adapter?(:OracleAdapter) - def test_update_all_ignores_order_without_limit_from_association - author = authors(:david) - assert_nothing_raised do - assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) - end - end - - def test_update_all_doesnt_ignore_order - assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error - test_update_with_order_succeeds = lambda do |order| - begin - Author.order(order).update_all("id = id + 1") - rescue ActiveRecord::ActiveRecordError - false - end - end - - if test_update_with_order_succeeds.call("id DESC") - assert_not test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception - else - # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead - assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do - test_update_with_order_succeeds.call("id DESC") - end - end - end - - def test_update_all_with_order_and_limit_updates_subset_only - author = authors(:david) - limited_posts = author.posts_sorted_by_id_limited - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:welcome).body - assert_not_equal "bulk update!", posts(:thinking).body - end - - def test_update_all_with_order_and_limit_and_offset_updates_subset_only - author = authors(:david) - limited_posts = author.posts_sorted_by_id_limited.offset(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:thinking).body - assert_not_equal "bulk update!", posts(:welcome).body - end - - def test_delete_all_with_order_and_limit_deletes_subset_only - author = authors(:david) - limited_posts = Post.where(author: author).order(:id).limit(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.delete_all - assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } - assert posts(:thinking) - end - - def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only - author = authors(:david) - limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.delete_all - assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } - assert posts(:welcome) - end - end + fixtures :topics, :companies, :developers, :accounts, :minimalistics, :authors, :author_addresses, :posts, :minivans def test_update_many topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } @@ -145,34 +70,6 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal Topic.count, Topic.delete_all end - def test_delete_all_with_joins_and_where_part_is_hash - pets = Pet.joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - - def test_delete_all_with_joins_and_where_part_is_not_hash - pets = Pet.joins(:toys).where("toys.name = ?", "Bone") - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - - def test_delete_all_with_left_joins - pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - - def test_delete_all_with_includes - pets = Pet.includes(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - def test_increment_attribute assert_equal 50, accounts(:signals37).credit_limit accounts(:signals37).increment! :credit_limit @@ -230,16 +127,9 @@ class PersistenceTest < ActiveRecord::TestCase assert_operator previously_written_on, :<, topic.written_on end - def test_destroy_all - conditions = "author_name = 'Mary'" - topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a - assert_not_empty topics_by_mary - - assert_difference("Topic.count", -topics_by_mary.size) do - destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) - assert_equal topics_by_mary, destroyed - assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" - end + def test_increment_with_no_arg + topic = topics(:first) + assert_raises(ArgumentError) { topic.increment! } end def test_destroy_many @@ -556,19 +446,17 @@ class PersistenceTest < ActiveRecord::TestCase end def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed - klass = Class.new(Topic) do - def self.name; "Topic"; end - end - topic = klass.create(title: "Another New Topic") - assert_queries(0) do + topic = Topic.create(title: "Another New Topic") + assert_no_queries do assert topic.update_attribute(:title, "Another New Topic") end end def test_update_does_not_run_sql_if_record_has_not_changed topic = Topic.create(title: "Another New Topic") - assert_queries(0) { assert topic.update(title: "Another New Topic") } - assert_queries(0) { assert topic.update(title: "Another New Topic") } + assert_no_queries do + assert topic.update(title: "Another New Topic") + end end def test_delete @@ -638,32 +526,6 @@ class PersistenceTest < ActiveRecord::TestCase assert_nil Topic.find(2).last_read end - def test_update_all_with_joins - pets = Pet.joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.update_all(name: "Bob") - end - - def test_update_all_with_left_joins - pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.update_all(name: "Bob") - end - - def test_update_all_with_includes - pets = Pet.includes(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.update_all(name: "Bob") - end - - def test_update_all_with_non_standard_table_name - assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0]) - assert_equal 0, WarehouseThing.find(1).value - end - def test_delete_new_record client = Client.new(name: "37signals") client.delete @@ -1185,21 +1047,19 @@ class PersistenceTest < ActiveRecord::TestCase ActiveRecord::Base.connection.disable_query_cache! end - class SaveTest < ActiveRecord::TestCase - def test_save_touch_false - pet = Pet.create!( - name: "Bob", - created_at: 1.day.ago, - updated_at: 1.day.ago) + def test_save_touch_false + parrot = Parrot.create!( + name: "Bob", + created_at: 1.day.ago, + updated_at: 1.day.ago) - created_at = pet.created_at - updated_at = pet.updated_at + created_at = parrot.created_at + updated_at = parrot.updated_at - pet.name = "Barb" - pet.save!(touch: false) - assert_equal pet.created_at, created_at - assert_equal pet.updated_at, updated_at - end + parrot.name = "Barb" + parrot.save!(touch: false) + assert_equal parrot.created_at, created_at + assert_equal parrot.updated_at, updated_at end def test_reset_column_information_resets_children diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 69be091869..565190c476 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -55,16 +55,6 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off end - private def with_temporary_connection_pool - old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) - new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec - ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool - - yield - ensure - ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool - end - def test_query_cache_across_threads with_temporary_connection_pool do begin @@ -200,7 +190,7 @@ class QueryCacheTest < ActiveRecord::TestCase Task.cache do assert_queries(2) { Task.find(1); Task.find(2) } end - assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) } + assert_no_queries { Task.find(1); Task.find(1); Task.find(2) } end end @@ -382,7 +372,7 @@ class QueryCacheTest < ActiveRecord::TestCase end # Check that if the same query is run again, no queries are executed - assert_queries(0) do + assert_no_queries do assert_equal 0, Post.where(title: "test").to_a.count end @@ -437,8 +427,9 @@ class QueryCacheTest < ActiveRecord::TestCase # Clear places where type information is cached Task.reset_column_information Task.initialize_find_by_cache + Task.define_attribute_methods - assert_queries(0) do + assert_no_queries do Task.find(1) end end @@ -495,6 +486,17 @@ class QueryCacheTest < ActiveRecord::TestCase end private + + def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool + + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end + def middleware(&app) executor = Class.new(ActiveSupport::Executor) ActiveRecord::QueryCache.install_executor_hooks executor diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b034fe3e3b..b630f782bc 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -61,9 +61,9 @@ module ActiveRecord def test_reaping_frequency_configuration spec = ActiveRecord::Base.connection_pool.spec.dup - spec.config[:reaping_frequency] = 100 + spec.config[:reaping_frequency] = "10.01" pool = ConnectionPool.new spec - assert_equal 100, pool.reaper.frequency + assert_equal 10.01, pool.reaper.frequency end def test_connection_pool_starts_reaper diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 3f3d41980c..a8030c2d64 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/comment" module ActiveRecord - module DelegationWhitelistTests + module ArrayDelegationTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -38,7 +38,7 @@ module ActiveRecord end class DelegationAssociationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target @@ -47,7 +47,7 @@ module ActiveRecord end class DelegationRelationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb new file mode 100644 index 0000000000..446d7621ea --- /dev/null +++ b/activerecord/test/cases/relation/delete_all_test.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/author" +require "models/post" +require "models/pet" +require "models/toy" + +class DeleteAllTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses, :posts, :pets, :toys + + def test_destroy_all + davids = Author.where(name: "David") + + # Force load + assert_equal [authors(:david)], davids.to_a + assert_predicate davids, :loaded? + + assert_difference("Author.count", -1) do + destroyed = davids.destroy_all + assert_equal [authors(:david)], destroyed + assert_predicate destroyed.first, :frozen? + end + + assert_equal [], davids.to_a + assert_predicate davids, :loaded? + end + + def test_delete_all + davids = Author.where(name: "David") + + assert_difference("Author.count", -1) { davids.delete_all } + assert_not_predicate davids, :loaded? + end + + def test_delete_all_loaded + davids = Author.where(name: "David") + + # Force load + assert_equal [authors(:david)], davids.to_a + assert_predicate davids, :loaded? + + assert_difference("Author.count", -1) { davids.delete_all } + + assert_equal [], davids.to_a + assert_predicate davids, :loaded? + end + + def test_delete_all_with_unpermitted_relation_raises_error + assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all } + end + + def test_delete_all_with_joins_and_where_part_is_hash + pets = Pet.joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + def test_delete_all_with_joins_and_where_part_is_not_hash + pets = Pet.joins(:toys).where("toys.name = ?", "Bone") + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + def test_delete_all_with_left_joins + pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + def test_delete_all_with_includes + pets = Pet.includes(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + unless current_adapter?(:OracleAdapter) + def test_delete_all_with_order_and_limit_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } + assert posts(:thinking) + end + + def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } + assert posts(:welcome) + end + end +end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index f53ef1fe35..224e4f39a8 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -121,6 +121,20 @@ class RelationMergingTest < ActiveRecord::TestCase relation = relation.merge(Post.from("posts")) assert_not_empty relation.from_clause end + + def test_merging_with_from_clause_on_different_class + assert Comment.joins(:post).merge(Post.from("posts")).first + end + + def test_merging_with_order_with_binds + relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"])) + assert_equal ["title LIKE '%suffix'"], relation.order_values + end + + def test_merging_with_order_without_binds + relation = Post.all.merge(Post.order(Arel.sql("title LIKE '%?'"))) + assert_equal ["title LIKE '%?'"], relation.order_values + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb index 0577e6bfdb..dec8a6925d 100644 --- a/activerecord/test/cases/relation/select_test.rb +++ b/activerecord/test/cases/relation/select_test.rb @@ -7,7 +7,7 @@ module ActiveRecord class SelectTest < ActiveRecord::TestCase fixtures :posts - def test_select_with_nil_agrument + def test_select_with_nil_argument expected = Post.select(:title).to_sql assert_equal expected, Post.select(nil).select(:title).to_sql end diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb new file mode 100644 index 0000000000..09c365f31b --- /dev/null +++ b/activerecord/test/cases/relation/update_all_test.rb @@ -0,0 +1,239 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/author" +require "models/category" +require "models/comment" +require "models/computer" +require "models/developer" +require "models/post" +require "models/person" +require "models/pet" +require "models/toy" +require "models/topic" +require "models/tag" +require "models/tagging" +require "models/warehouse_thing" + +class UpdateAllTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses, :comments, :developers, :posts, :people, :pets, :toys, :tags, :taggings, "warehouse-things" + + class TopicWithCallbacks < ActiveRecord::Base + self.table_name = :topics + cattr_accessor :topic_count + before_update { |topic| topic.author_name = "David" if topic.author_name.blank? } + after_update { |topic| topic.class.topic_count = topic.class.count } + end + + def test_update_all_with_scope + tag = Tag.first + Post.tagged_with(tag.id).update_all(title: "rofl") + posts = Post.tagged_with(tag.id).all.to_a + assert_operator posts.length, :>, 0 + posts.each { |post| assert_equal "rofl", post.title } + end + + def test_update_all_with_non_standard_table_name + assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0]) + assert_equal 0, WarehouseThing.find(1).value + end + + def test_update_all_with_blank_argument + assert_raises(ArgumentError) { Comment.update_all({}) } + end + + def test_update_all_with_joins + pets = Pet.joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_left_joins + pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_includes + pets = Pet.includes(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_joins_and_limit + comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1) + assert_equal 1, comments.update_all(post_id: posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + end + + def test_update_all_with_joins_and_limit_and_order + comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1) + assert_equal 1, comments.update_all(post_id: posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + assert_equal posts(:welcome), comments(:more_greetings).post + end + + def test_update_all_with_joins_and_offset + all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id) + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) + end + + def test_update_all_with_joins_and_offset_and_order + all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id") + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) + assert_equal posts(:thinking), comments(:more_greetings).post + assert_equal posts(:welcome), comments(:greetings).post + end + + def test_update_counters_with_joins + assert_nil pets(:parrot).integer + + Pet.joins(:toys).where(toys: { name: "Bone" }).update_counters(integer: 1) + + assert_equal 1, pets(:parrot).reload.integer + end + + def test_touch_all_updates_records_timestamps + david = developers(:david) + david_previously_updated_at = david.updated_at + jamis = developers(:jamis) + jamis_previously_updated_at = jamis.updated_at + Developer.where(name: "David").touch_all + + assert_not_equal david_previously_updated_at, david.reload.updated_at + assert_equal jamis_previously_updated_at, jamis.reload.updated_at + end + + def test_touch_all_with_custom_timestamp + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + Developer.where(name: "David").touch_all(:created_at) + developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + end + + def test_touch_all_with_given_time + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + new_time = Time.utc(2015, 2, 16, 4, 54, 0) + Developer.where(name: "David").touch_all(:created_at, time: new_time) + developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + assert_equal new_time, developer.created_at + assert_equal new_time, developer.updated_at + end + + def test_touch_all_updates_locking_column + person = people(:david) + + assert_difference -> { person.reload.lock_version }, +1 do + Person.where(first_name: "David").touch_all + end + end + + def test_update_on_relation + topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil + topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil + topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) + topics.update(title: "adequaterecord") + + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + + assert_equal "adequaterecord", topic1.reload.title + assert_equal "adequaterecord", topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal "David", topic1.reload.author_name + assert_equal "David", topic2.reload.author_name + end + + def test_update_with_ids_on_relation + topic1 = TopicWithCallbacks.create!(title: "arel", author_name: nil) + topic2 = TopicWithCallbacks.create!(title: "activerecord", author_name: nil) + topics = TopicWithCallbacks.none + topics.update( + [topic1.id, topic2.id], + [{ title: "adequaterecord" }, { title: "adequaterecord" }] + ) + + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + + assert_equal "adequaterecord", topic1.reload.title + assert_equal "adequaterecord", topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal "David", topic1.reload.author_name + assert_equal "David", topic2.reload.author_name + end + + def test_update_on_relation_passing_active_record_object_is_not_permitted + topic = Topic.create!(title: "Foo", author_name: nil) + assert_raises(ArgumentError) do + Topic.where(id: topic.id).update(topic, title: "Bar") + end + end + + # Oracle UPDATE does not support ORDER BY + unless current_adapter?(:OracleAdapter) + def test_update_all_ignores_order_without_limit_from_association + author = authors(:david) + assert_nothing_raised do + assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) + end + end + + def test_update_all_doesnt_ignore_order + assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error + test_update_with_order_succeeds = lambda do |order| + begin + Author.order(order).update_all("id = id + 1") + rescue ActiveRecord::ActiveRecordError + false + end + end + + if test_update_with_order_succeeds.call("id DESC") + # test that this wasn't a fluke and using an incorrect order results in an exception + assert_not test_update_with_order_succeeds.call("id ASC") + else + # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do + test_update_with_order_succeeds.call("id DESC") + end + end + end + + def test_update_all_with_order_and_limit_updates_subset_only + author = authors(:david) + limited_posts = author.posts_sorted_by_id_limited + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:welcome).body + assert_not_equal "bulk update!", posts(:thinking).body + end + + def test_update_all_with_order_and_limit_and_offset_updates_subset_only + author = authors(:david) + limited_posts = author.posts_sorted_by_id_limited.offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:thinking).body + assert_not_equal "bulk update!", posts(:welcome).body + end + end +end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fbeb617b29..68161f6a84 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -232,7 +232,7 @@ module ActiveRecord assert_equal 3, nb_inner_join, "Wrong amount of INNER JOIN in query" # using `\W` as the column separator - assert queries.any? { |sql| %r[INNER\s+JOIN\s+#{Author.quoted_table_name}\s+\Wauthors_categorizations\W]i.match?(sql) }, "Should be aliasing the child INNER JOINs in query" + assert queries.any? { |sql| %r[INNER\s+JOIN\s+#{Regexp.escape(Author.quoted_table_name)}\s+\Wauthors_categorizations\W]i.match?(sql) }, "Should be aliasing the child INNER JOINs in query" end def test_relation_with_merged_joins_aliased_works diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5412ab5def..e471ee8039 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -9,6 +9,7 @@ require "models/comment" require "models/author" require "models/entrant" require "models/developer" +require "models/project" require "models/person" require "models/computer" require "models/reply" @@ -28,13 +29,6 @@ require "models/subscriber" class RelationTest < ActiveRecord::TestCase fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans - class TopicWithCallbacks < ActiveRecord::Base - self.table_name = :topics - cattr_accessor :topic_count - before_update { |topic| topic.author_name = "David" if topic.author_name.blank? } - after_update { |topic| topic.class.topic_count = topic.class.count } - end - def test_do_not_double_quote_string_id van = Minivan.last assert van @@ -862,45 +856,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal authors(:bob), authors.last end - def test_destroy_all - davids = Author.where(name: "David") - - # Force load - assert_equal [authors(:david)], davids.to_a - assert_predicate davids, :loaded? - - assert_difference("Author.count", -1) { davids.destroy_all } - - assert_equal [], davids.to_a - assert_predicate davids, :loaded? - end - - def test_delete_all - davids = Author.where(name: "David") - - assert_difference("Author.count", -1) { davids.delete_all } - assert_not_predicate davids, :loaded? - end - - def test_delete_all_loaded - davids = Author.where(name: "David") - - # Force load - assert_equal [authors(:david)], davids.to_a - assert_predicate davids, :loaded? - - assert_difference("Author.count", -1) { davids.delete_all } - - assert_equal [], davids.to_a - assert_predicate davids, :loaded? - end - - def test_delete_all_with_unpermitted_relation_raises_error - assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all } - end - def test_select_with_aggregates posts = Post.select(:title, :body) @@ -985,14 +940,6 @@ class RelationTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 11, posts.load.size } end - def test_update_all_with_scope - tag = Tag.first - Post.tagged_with(tag.id).update_all title: "rofl" - list = Post.tagged_with(tag.id).all.to_a - assert_operator list.length, :>, 0 - list.each { |post| assert_equal "rofl", post.title } - end - def test_count_explicit_columns Post.update_all(comments_count: nil) posts = Post.all @@ -1405,6 +1352,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal "cock", hens.new.name end + def test_create_with_nested_attributes + assert_difference("Project.count", 1) do + developers = Developer.where(name: "Aaron") + developers = developers.create_with( + projects_attributes: [{ name: "p1" }] + ) + developers.create! + end + end + def test_except relation = Post.where(author_id: 1).order("id ASC").limit(1) assert_equal [posts(:welcome)], relation.to_a @@ -1486,114 +1443,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal authors(:david), Author.order("id DESC , name DESC").last end - def test_update_all_with_blank_argument - assert_raises(ArgumentError) { Comment.update_all({}) } - end - - def test_update_all_with_joins - comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id) - count = comments.count - - assert_equal count, comments.update_all(post_id: posts(:thinking).id) - assert_equal posts(:thinking), comments(:greetings).post - end - - def test_update_all_with_joins_and_limit - comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1) - assert_equal 1, comments.update_all(post_id: posts(:thinking).id) - end - - def test_update_all_with_joins_and_limit_and_order - comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1) - assert_equal 1, comments.update_all(post_id: posts(:thinking).id) - assert_equal posts(:thinking), comments(:greetings).post - assert_equal posts(:welcome), comments(:more_greetings).post - end - - def test_update_all_with_joins_and_offset - all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id) - count = all_comments.count - comments = all_comments.offset(1) - - assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) - end - - def test_update_all_with_joins_and_offset_and_order - all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id") - count = all_comments.count - comments = all_comments.offset(1) - - assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) - assert_equal posts(:thinking), comments(:more_greetings).post - assert_equal posts(:welcome), comments(:greetings).post - end - - def test_touch_all_updates_records_timestamps - david = developers(:david) - david_previously_updated_at = david.updated_at - jamis = developers(:jamis) - jamis_previously_updated_at = jamis.updated_at - Developer.where(name: "David").touch_all - - assert_not_equal david_previously_updated_at, david.reload.updated_at - assert_equal jamis_previously_updated_at, jamis.reload.updated_at - end - - def test_touch_all_with_custom_timestamp - developer = developers(:david) - previously_created_at = developer.created_at - previously_updated_at = developer.updated_at - Developer.where(name: "David").touch_all(:created_at) - developer = developer.reload - - assert_not_equal previously_created_at, developer.created_at - assert_not_equal previously_updated_at, developer.updated_at - end - - def test_touch_all_with_given_time - developer = developers(:david) - previously_created_at = developer.created_at - previously_updated_at = developer.updated_at - new_time = Time.utc(2015, 2, 16, 4, 54, 0) - Developer.where(name: "David").touch_all(:created_at, time: new_time) - developer = developer.reload - - assert_not_equal previously_created_at, developer.created_at - assert_not_equal previously_updated_at, developer.updated_at - assert_equal new_time, developer.created_at - assert_equal new_time, developer.updated_at - end - - def test_touch_all_updates_locking_column - person = people(:david) - - assert_difference -> { person.reload.lock_version }, +1 do - Person.where(first_name: "David").touch_all - end - end - - def test_update_on_relation - topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil - topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil - topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) - topics.update(title: "adequaterecord") - - assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count - - assert_equal "adequaterecord", topic1.reload.title - assert_equal "adequaterecord", topic2.reload.title - # Testing that the before_update callbacks have run - assert_equal "David", topic1.reload.author_name - assert_equal "David", topic2.reload.author_name - end - - def test_update_on_relation_passing_active_record_object_is_not_permitted - topic = Topic.create!(title: "Foo", author_name: nil) - assert_raises(ArgumentError) do - Topic.where(id: topic.id).update(topic, title: "Bar") - end - end - def test_distinct tag1 = Tag.create(name: "Foo") tag2 = Tag.create(name: "Foo") @@ -1963,6 +1812,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end + def test_relation_with_private_kernel_method + accounts = Account.all + assert_equal [accounts(:signals37)], accounts.open + assert_equal [accounts(:signals37)], accounts.available + + sub_accounts = SubAccount.all + assert_equal [accounts(:signals37)], sub_accounts.open + assert_equal [accounts(:signals37)], sub_accounts.available + end + test "#skip_query_cache!" do Post.cache do assert_queries(1) do diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 68fcafb682..825aee2423 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -21,12 +21,22 @@ module ActiveRecord assert_equal 3, result.length end - test "to_hash returns row_hashes" do + test "to_a returns row_hashes" do assert_equal [ { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" }, { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, - ], result.to_hash + ], result.to_a + end + + test "to_hash (deprecated) returns row_hashes" do + assert_deprecated do + assert_equal [ + { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, + { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" }, + { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, + ], result.to_hash + end end test "first returns first row as a hash" do diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 75b68b521c..dda3efa47c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -226,6 +226,23 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.float\s+"temperature"$}, output end + if ActiveRecord::Base.connection.supports_expression_index? + def test_schema_dump_expression_indices + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + index_definition.sub!(/, name: "company_expression_index"\z/, "") + + if current_adapter?(:PostgreSQLAdapter) + assert_match %r{CASE.+lower\(\(name\)::text\).+END\) DESC"\z}i, index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_match %r{CASE.+lower\(`name`\).+END\) DESC"\z}i, index_definition + elsif current_adapter?(:SQLite3Adapter) + assert_match %r{CASE.+lower\(name\).+END\) DESC"\z}i, index_definition + else + assert false + end + end + end + if current_adapter?(:Mysql2Adapter) def test_schema_dump_includes_length_for_mysql_binary_fields output = standard_dump @@ -278,11 +295,6 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output end - def test_schema_dump_expression_indices - index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition - end - def test_schema_dump_interval_type output = dump_table_schema "postgresql_times" assert_match %r{t\.interval\s+"time_interval"$}, output diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index e3a34aa50d..6281712df6 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/post" require "models/comment" require "models/developer" +require "models/project" require "models/computer" require "models/vehicle" require "models/cat" @@ -366,6 +367,21 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal "Jamis", jamis.name end + def test_create_with_takes_precedence_over_where + developer = Developer.where(name: nil).create_with(name: "Aaron").new + assert_equal "Aaron", developer.name + end + + def test_create_with_nested_attributes + assert_difference("Project.count", 1) do + Developer.create_with( + projects_attributes: [{ name: "p1" }] + ).scoping do + Developer.create!(name: "Aaron") + end + end + end + # FIXME: I don't know if this is *desired* behavior, but it is *today's* # behavior. def test_create_with_empty_hash_will_not_reset diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 4214f347fb..f707951a16 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -489,7 +489,7 @@ class NamedScopingTest < ActiveRecord::TestCase [:public_method, :protected_method, :private_method].each do |reserved_method| assert Topic.respond_to?(reserved_method, true) assert_called(ActiveRecord::Base.logger, :warn) do - silence_warnings { Topic.scope(reserved_method, -> {}) } + silence_warnings { Topic.scope(reserved_method, -> { }) } end end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 544adc9b39..b4f4379e5e 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -236,8 +236,8 @@ class RelationScopingTest < ActiveRecord::TestCase SpecialComment.unscoped.created end - assert_nil Comment.current_scope - assert_nil SpecialComment.current_scope + assert_nil Comment.send(:current_scope) + assert_nil SpecialComment.send(:current_scope) end def test_scoping_respects_current_class diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 4cd4515c3b..1192b30b14 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -322,7 +322,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.create!(content: {}) topic2 = Topic.create!(content: nil) - assert_equal [topic, topic2], Topic.where(content: nil) + assert_equal [topic, topic2], Topic.where(content: nil).sort_by(&:id) end def test_nil_is_always_persisted_as_null diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index b68f0033d9..9be5356901 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -66,7 +66,7 @@ class SuppressorTest < ActiveRecord::TestCase def test_suppresses_when_nested_multiple_times assert_no_difference -> { Notification.count } do Notification.suppress do - Notification.suppress {} + Notification.suppress { } Notification.create Notification.create! Notification.new.save diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index a6efe3fa5e..3fd1813d64 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -46,35 +46,46 @@ module ActiveRecord class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment - assert_not_includes protected_environments, current_env - # Assert no error - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - - ActiveRecord::Base.protected_environments = [current_env] - assert_raise(ActiveRecord::ProtectedEnvironmentError) do + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + times: 6, + returns: 1 + ) do + assert_not_includes protected_environments, current_env + # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments = [current_env] + + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end ensure ActiveRecord::Base.protected_environments = protected_environments end def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment - assert_not_includes protected_environments, current_env - # Assert no error - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - - ActiveRecord::Base.protected_environments = [current_env.to_sym] - assert_raise(ActiveRecord::ProtectedEnvironmentError) do + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + times: 6, + returns: 1 + ) do + assert_not_includes protected_environments, current_env + # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments = [current_env.to_sym] + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end ensure ActiveRecord::Base.protected_environments = protected_environments @@ -82,10 +93,14 @@ module ActiveRecord def test_raises_an_error_if_no_migrations_have_been_made ActiveRecord::InternalMetadata.stub(:table_exists?, false) do - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - - assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + returns: 1 + ) do + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end end end @@ -213,18 +228,17 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - # To refrain from connecting to a newly created empty DB in - # sqlite3_mem tests - ActiveRecord::Base.connection_handler.stub( - :establish_connection, - nil - ) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + # To refrain from connecting to a newly created empty DB in + # sqlite3_mem tests + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -233,7 +247,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-host/prod-db" } } end @@ -256,7 +270,7 @@ module ActiveRecord assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, - ["url" => "prod-db-url"], + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], ) do ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("production") @@ -315,13 +329,15 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -330,7 +346,7 @@ module ActiveRecord @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } } end @@ -357,8 +373,8 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks, :create, [ - ["url" => "prod-db-url"], - ["url" => "secondary-prod-db-url"] + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] ] ) do ActiveRecord::Tasks::DatabaseTasks.create_current( @@ -426,13 +442,15 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -465,7 +483,7 @@ module ActiveRecord def test_ignores_configurations_without_databases @configurations[:development]["database"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -475,7 +493,7 @@ module ActiveRecord def test_ignores_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -485,7 +503,7 @@ module ActiveRecord def test_warning_for_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do ActiveRecord::Tasks::DatabaseTasks.drop_all assert_match "This task only modifies local databases. my-db is on a remote host.", @@ -496,7 +514,7 @@ module ActiveRecord def test_drops_configurations_with_local_ip @configurations[:development]["host"] = "127.0.0.1" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -506,7 +524,7 @@ module ActiveRecord def test_drops_configurations_with_local_host @configurations[:development]["host"] = "localhost" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -516,12 +534,22 @@ module ActiveRecord def test_drops_configurations_with_blank_hosts @configurations[:development]["host"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end end end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase @@ -529,12 +557,12 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-host/prod-db" } } end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, ["database" => "test-db"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -545,9 +573,9 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, - ["url" => "prod-db-url"]) do + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new("production") ) @@ -556,7 +584,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -576,7 +604,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -593,6 +621,16 @@ module ActiveRecord ensure ENV["RAILS_ENV"] = old_env end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase @@ -600,12 +638,12 @@ module ActiveRecord @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } } end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -622,13 +660,13 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, [ - ["url" => "prod-db-url"], - ["url" => "secondary-prod-db-url"] + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] ] ) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -639,7 +677,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -661,7 +699,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -680,10 +718,20 @@ module ActiveRecord ensure ENV["RAILS_ENV"] = old_env end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end if current_adapter?(:SQLite3Adapter) && !in_memory_db? - class DatabaseTasksMigrateTest < ActiveRecord::TestCase + class DatabaseTasksMigrationTestCase < ActiveRecord::TestCase self.use_transactional_tests = false # Use a memory db here to avoid having to rollback at the end @@ -703,7 +751,9 @@ module ActiveRecord @conn.release_connection if @conn ActiveRecord::Base.establish_connection :arunit end + end + class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase def test_migrate_set_and_unset_verbose_and_version_env_vars verbose, version = ENV["VERBOSE"], ENV["VERSION"] ENV["VERSION"] = "2" @@ -764,6 +814,26 @@ module ActiveRecord end end end + + class DatabaseTasksMigrateStatusTest < DatabaseTasksMigrationTestCase + def test_migrate_status_table + ActiveRecord::SchemaMigration.create_table + output = capture_migration_status + assert_match(/database: :memory:/, output) + assert_match(/down 001 Valid people have last names/, output) + assert_match(/down 002 We need reminders/, output) + assert_match(/down 003 Innocent jointable/, output) + ActiveRecord::SchemaMigration.drop_table + end + + private + + def capture_migration_status + capture(:stdout) do + ActiveRecord::Tasks::DatabaseTasks.migrate_status + end + end + end end class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase @@ -833,37 +903,44 @@ module ActiveRecord class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase def test_purges_current_environment_database + old_configurations = ActiveRecord::Base.configurations configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, "production" => { "database" => "prod-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :purge, - ["database" => "prod-db"] - ) do - assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do - ActiveRecord::Tasks::DatabaseTasks.purge_current("production") - end + + ActiveRecord::Base.configurations = configurations + + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "prod-db"] + ) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do + ActiveRecord::Tasks::DatabaseTasks.purge_current("production") end end + ensure + ActiveRecord::Base.configurations = old_configurations end end class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase def test_purge_all_local_configurations + old_configurations = ActiveRecord::Base.configurations configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :purge, - ["database" => "my-db"] - ) do - ActiveRecord::Tasks::DatabaseTasks.purge_all - end + ActiveRecord::Base.configurations = configurations + + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "my-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge_all end + ensure + ActiveRecord::Base.configurations = old_configurations end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index eeb4222d97..552e623fd4 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -152,10 +152,14 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_mysql_database - with_stubbed_connection_establish_connection do - ActiveRecord::Base.expects(:establish_connection).with @configuration - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [@configuration] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end end @@ -196,10 +200,14 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_the_appropriate_database - with_stubbed_connection_establish_connection do - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [@configuration] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end end @@ -264,7 +272,7 @@ if current_adapter?(:Mysql2Adapter) def test_db_retrieves_collation ActiveRecord::Base.stub(:connection, @connection) do - assert_called_with(@connection, :collation) do + assert_called(@connection, :collation) do ActiveRecord::Tasks::DatabaseTasks.collation @configuration end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 00005e7a0d..065ba7734c 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -112,7 +112,7 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stub(:connection, @connection) do ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end @@ -166,12 +166,17 @@ if current_adapter?(:PostgreSQLAdapter) def test_establishes_connection_to_postgresql_database ActiveRecord::Base.stub(:connection, @connection) do - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end end @@ -491,7 +496,7 @@ if current_adapter?(:PostgreSQLAdapter) assert_called_with( Kernel, :system, - ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]], + ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]], returns: true ) do ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) @@ -500,7 +505,7 @@ if current_adapter?(:PostgreSQLAdapter) def test_structure_load_with_extra_flags filename = "awesome-file.sql" - expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]] + expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, "--noop", @configuration["database"]] assert_called_with(Kernel, :system, expected_command, returns: true) do with_structure_load_flags(["--noop"]) do @@ -514,7 +519,7 @@ if current_adapter?(:PostgreSQLAdapter) assert_called_with( Kernel, :system, - ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]], + ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]], returns: true ) do ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 7eb062b456..c1092b97c1 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -47,9 +47,9 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_file_does_nothing File.stub(:exist?, true) do - ActiveRecord::Base.expects(:establish_connection).never - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + assert_not_called(ActiveRecord::Base, :establish_connection) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end end @@ -62,7 +62,7 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_error_prints_message ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 409b07e56c..40947767f3 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -31,6 +31,7 @@ module ActiveRecord end def capture_sql + ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log yield SQLCounter.log_all.dup @@ -48,6 +49,7 @@ module ActiveRecord def assert_queries(num = 1, options = {}) ignore_none = options.fetch(:ignore_none) { num == :any } + ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log x = yield the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 925a4609a2..cd3d5ed7d1 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -100,7 +100,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_hit_the_db invoice = Invoice.create! - assert_queries(0) do + assert_no_queries do invoice.touch_later end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index c0be45eee7..aa6b7915a2 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -591,6 +591,17 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:before_commit, :after_commit], @topic.history end + def test_commit_run_transactions_callbacks_with_nested_transactions + @topic.transaction do + @topic.transaction(requires_new: true) do + @topic.content = "foo" + @topic.save! + @topic.class.connection.add_transaction_record(@topic) + end + end + assert_equal [:before_commit, :after_commit], @topic.history + end + def test_rollback_does_not_run_transactions_callbacks_without_enrollment @topic.transaction do @topic.content = "foo" diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index eaafd13360..2932969412 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -11,7 +11,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation? test "setting the isolation level raises an error" do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) {} + Tag.transaction(isolation: :serializable) { Tag.connection.materialize_transactions } end end end @@ -90,7 +90,7 @@ else test "setting isolation when joining a transaction raises an error" do Tag.transaction do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) {} + Tag.transaction(isolation: :serializable) { } end end end @@ -98,7 +98,7 @@ else test "setting isolation when starting a nested transaction raises error" do Tag.transaction do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(requires_new: true, isolation: :serializable) {} + Tag.transaction(requires_new: true, isolation: :serializable) { } end end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 46463ac414..50740054f7 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -575,7 +575,7 @@ class TransactionTest < ActiveRecord::TestCase assert_called(Topic.connection, :rollback_db_transaction) do e = assert_raise RuntimeError do Topic.transaction do - # do nothing + Topic.connection.materialize_transactions end end assert_equal "OH NOES", e.message @@ -930,7 +930,7 @@ class TransactionTest < ActiveRecord::TestCase klass = Class.new(ActiveRecord::Base) do self.table_name = "transaction_without_primary_keys" - after_commit {} # necessary to trigger the has_transactional_callbacks branch + after_commit { } # necessary to trigger the has_transactional_callbacks branch end assert_no_difference(-> { klass.count }) do @@ -943,6 +943,76 @@ class TransactionTest < ActiveRecord::TestCase connection.drop_table "transaction_without_primary_keys", if_exists: true end + def test_empty_transaction_is_not_materialized + assert_no_queries do + Topic.transaction { } + end + end + + def test_unprepared_statement_materializes_transaction + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.where("1=1").first } + end + end + + if ActiveRecord::Base.connection.prepared_statements + def test_prepared_statement_materializes_transaction + Topic.first + + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.first } + end + end + end + + def test_savepoint_does_not_materialize_transaction + assert_no_queries do + Topic.transaction do + Topic.transaction(requires_new: true) { } + end + end + end + + def test_raising_does_not_materialize_transaction + assert_raise(RuntimeError) do + assert_no_queries do + Topic.transaction { raise } + end + end + end + + def test_accessing_raw_connection_materializes_transaction + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.connection.raw_connection } + end + end + + def test_accessing_raw_connection_disables_lazy_transactions + Topic.connection.raw_connection + + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { } + end + end + + def test_checking_in_connection_reenables_lazy_transactions + connection = Topic.connection_pool.checkout + connection.raw_connection + Topic.connection_pool.checkin connection + + assert_no_queries do + connection.transaction { } + end + end + + def test_transactions_can_be_manually_materialized + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction do + Topic.connection.materialize_transactions + end + end + end + private %w(validation save destroy).each do |filter| diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb index f3699c11a2..1ce515a90c 100644 --- a/activerecord/test/cases/type/type_map_test.rb +++ b/activerecord/test/cases/type/type_map_test.rb @@ -32,7 +32,7 @@ module ActiveRecord end def test_fuzzy_lookup - string = String.new + string = +"" mapping = TypeMap.new mapping.register_type(/varchar/i, string) @@ -41,7 +41,7 @@ module ActiveRecord end def test_aliasing_types - string = String.new + string = +"" mapping = TypeMap.new mapping.register_type(/string/i, string) @@ -73,7 +73,7 @@ module ActiveRecord end def test_register_proc - string = String.new + string = +"" binary = Binary.new mapping = TypeMap.new diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a33877f43a..66763c727f 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -3,11 +3,11 @@ require "cases/helper" require "models/topic" require "models/reply" -require "models/person" require "models/developer" require "models/computer" require "models/parrot" require "models/company" +require "models/price_estimate" class ValidationsTest < ActiveRecord::TestCase fixtures :topics, :developers @@ -183,6 +183,22 @@ class ValidationsTest < ActiveRecord::TestCase assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid? end + def test_numericality_validator_wont_be_affected_by_custom_getter + price_estimate = PriceEstimate.new(price: 50) + + assert_equal "$50.00", price_estimate.price + assert_equal 50, price_estimate.price_before_type_cast + assert_equal 50, price_estimate.read_attribute(:price) + + assert_predicate price_estimate, :price_came_from_user? + assert_predicate price_estimate, :valid? + + price_estimate.save! + + assert_not_predicate price_estimate, :price_came_from_user? + assert_predicate price_estimate, :valid? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 4bcb2aeea6..18347cd07d 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -54,11 +54,12 @@ connections: mysql2: arunit: username: rails - encoding: utf8 - collation: utf8_unicode_ci + encoding: utf8mb4 + collation: utf8mb4_unicode_ci arunit2: username: rails - encoding: utf8 + encoding: utf8mb4 + collation: utf8mb4_general_ci oracle: arunit: diff --git a/activerecord/test/fixtures/citations.yml b/activerecord/test/fixtures/citations.yml new file mode 100644 index 0000000000..396099621c --- /dev/null +++ b/activerecord/test/fixtures/citations.yml @@ -0,0 +1,5 @@ +<% 65536.times do |i| %> +fixture_no_<%= i %>: + id: <%= i %> + book2_id: <%= i*i %> +<% end %> diff --git a/activerecord/test/models/account.rb b/activerecord/test/models/account.rb index 0c3cd45a81..639e395743 100644 --- a/activerecord/test/models/account.rb +++ b/activerecord/test/models/account.rb @@ -11,9 +11,8 @@ class Account < ActiveRecord::Base end # Test private kernel method through collection proxy using has_many. - def self.open - where("firm_name = ?", "37signals") - end + scope :open, -> { where("firm_name = ?", "37signals") } + scope :available, -> { open } before_destroy do |account| if account.firm @@ -32,3 +31,11 @@ class Account < ActiveRecord::Base "Sir, yes sir!" end end + +class SubAccount < Account + def self.instantiate_instance_of(klass, attributes, column_types = {}, &block) + klass = superclass + super + end + private_class_method :instantiate_instance_of +end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 75932c7eb6..8b5a2fa0c8 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -81,7 +81,7 @@ class Author < ActiveRecord::Base after_add: [:log_after_adding, Proc.new { |o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}" }] has_many :unchangeable_posts, class_name: "Post", before_add: :raise_exception, after_add: :log_after_adding - has_many :categorizations, -> {} + has_many :categorizations, -> { } has_many :categories, through: :categorizations has_many :named_categories, through: :categorizations diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb index 3d786f27eb..cee3d18173 100644 --- a/activerecord/test/models/citation.rb +++ b/activerecord/test/models/citation.rb @@ -2,4 +2,5 @@ class Citation < ActiveRecord::Base belongs_to :reference_of, class_name: "Book", foreign_key: :book2_id + has_many :citations end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index d4d5275b78..838f515aad 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -122,6 +122,12 @@ class RestrictedWithErrorFirm < Company has_many :companies, -> { order("id") }, foreign_key: "client_of", dependent: :restrict_with_error end +class Agency < Firm + has_many :projects, foreign_key: :firm_id + + accepts_nested_attributes_for :projects +end + class Client < Company belongs_to :firm, foreign_key: "client_of" belongs_to :firm_with_basic_id, class_name: "Firm", foreign_key: "firm_id" @@ -204,4 +210,12 @@ end class VerySpecialClient < SpecialClient end +class NewlyContractedCompany < Company + has_many :new_contracts, foreign_key: "company_id" + + before_save do + self.new_contracts << NewContract.new + end +end + require "models/account" diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index f273badd85..3f663375c4 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -20,3 +20,7 @@ class Contract < ActiveRecord::Base @bye_count += 1 end end + +class NewContract < Contract + validates :company_id, presence: true +end diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb index 0c84a40de2..4b4a276a98 100644 --- a/activerecord/test/models/country.rb +++ b/activerecord/test/models/country.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class Country < ActiveRecord::Base - self.primary_key = :country_id - has_and_belongs_to_many :treaties end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index ba9ddb8c6a..3bb5316eca 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -20,6 +20,12 @@ class Parrot < ActiveRecord::Base def increment_updated_count self.updated_count += 1 end + + def self.delete_all(*) + connection.delete("DELETE FROM parrots_pirates") + connection.delete("DELETE FROM parrots_treasures") + super + end end class LiveParrot < Parrot diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 640cdb33b4..710a75ad30 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -297,8 +297,6 @@ end class FakeKlass extend ActiveRecord::Delegation::DelegateCache - inherited self - class << self def connection Post.connection @@ -324,7 +322,7 @@ class FakeKlass table[name] end - def enforce_raw_sql_whitelist(*args) + def disallow_raw_sql!(*args) # noop end @@ -335,5 +333,11 @@ class FakeKlass def predicate_builder Post.predicate_builder end + + def base_class? + true + end end + + inherited self end diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb index f1f88d8d8d..669d0991f7 100644 --- a/activerecord/test/models/price_estimate.rb +++ b/activerecord/test/models/price_estimate.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true class PriceEstimate < ActiveRecord::Base + include ActiveSupport::NumberHelper + belongs_to :estimate_of, polymorphic: true belongs_to :thing, polymorphic: true + + validates_numericality_of :price + + def price + number_to_currency super + end end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 0ea110f4f8..0807bcf875 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -9,6 +9,10 @@ class Reply < Topic has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id" end +class SillyReply < Topic + belongs_to :reply, foreign_key: "parent_id", counter_cache: :replies_count +end + class UniqueReply < Reply belongs_to :topic, foreign_key: "parent_id", counter_cache: true validates_uniqueness_of :content, scope: "parent_id" @@ -54,10 +58,6 @@ class WrongReply < Reply end end -class SillyReply < Reply - belongs_to :reply, foreign_key: "parent_id", counter_cache: :replies_count -end - module Web class Reply < Web::Topic belongs_to :topic, foreign_key: "parent_id", counter_cache: true, class_name: "Web::Topic" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 72699046f9..03430154db 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -24,7 +24,7 @@ class Topic < ActiveRecord::Base end scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> {} do + scope :anonymous_extension, -> { } do def one 1 end @@ -138,6 +138,10 @@ class BlankTopic < Topic end end +class TitlePrimaryKeyTopic < Topic + self.primary_key = :title +end + module Web class Topic < ActiveRecord::Base has_many :replies, dependent: :destroy, foreign_key: "parent_id", class_name: "Web::Reply" diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb index 5c1d75aa09..b87a757d2a 100644 --- a/activerecord/test/models/treaty.rb +++ b/activerecord/test/models/treaty.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class Treaty < ActiveRecord::Base - self.primary_key = :treaty_id - has_and_belongs_to_many :countries end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 8371ba9528..ccca9a2d9b 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -14,6 +14,16 @@ ActiveRecord::Schema.define do end end + create_table :defaults, force: true do |t| + t.date :fixed_date, default: "2004-01-01" + t.datetime :fixed_time, default: "2004-01-01 00:00:00" + t.column :char1, "char(1)", default: "Y" + t.string :char2, limit: 50, default: "a varchar field" + if supports_default_expression? + t.binary :uuid, limit: 36, default: -> { "(uuid())" } + end + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 @@ -29,7 +39,7 @@ ActiveRecord::Schema.define do t.index :var_binary end - create_table :key_tests, force: true, options: "ENGINE=MyISAM" do |t| + create_table :key_tests, force: true do |t| t.string :awesome t.string :pizza t.string :snacks @@ -39,8 +49,8 @@ ActiveRecord::Schema.define do end create_table :collation_tests, id: false, force: true do |t| - t.string :string_cs_column, limit: 1, collation: "utf8_bin" - t.string :string_ci_column, limit: 1, collation: "utf8_general_ci" + t.string :string_cs_column, limit: 1, collation: "utf8mb4_bin" + t.string :string_ci_column, limit: 1, collation: "utf8mb4_general_ci" t.binary :binary_column, limit: 1 end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 266e55f682..7034c773d2 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -93,7 +93,7 @@ ActiveRecord::Schema.define do t.integer :pirate_id end - create_table :books, force: true do |t| + create_table :books, id: :integer, force: true do |t| t.references :author t.string :format t.column :name, :string @@ -158,8 +158,9 @@ ActiveRecord::Schema.define do end create_table :citations, force: true do |t| - t.column :book1_id, :integer - t.column :book2_id, :integer + t.references :book1 + t.references :book2 + t.references :citation end create_table :clubs, force: true do |t| @@ -215,7 +216,7 @@ ActiveRecord::Schema.define do t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree - t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index? + t.index "(CASE WHEN rating > 0 THEN lower(name) END) DESC", name: "company_expression_index" if supports_expression_index? end create_table :content, force: true do |t| @@ -600,33 +601,55 @@ ActiveRecord::Schema.define do t.integer :non_poly_two_id end - create_table :parrots, force: true do |t| - t.column :name, :string - t.column :color, :string - t.column :parrot_sti_class, :string - t.column :killer_id, :integer - t.column :updated_count, :integer, default: 0 - if subsecond_precision_supported? - t.column :created_at, :datetime, precision: 0 - t.column :created_on, :datetime, precision: 0 - t.column :updated_at, :datetime, precision: 0 - t.column :updated_on, :datetime, precision: 0 - else - t.column :created_at, :datetime - t.column :created_on, :datetime - t.column :updated_at, :datetime - t.column :updated_on, :datetime + disable_referential_integrity do + create_table :parrots, force: :cascade do |t| + t.string :name + t.string :color + t.string :parrot_sti_class + t.integer :killer_id + t.integer :updated_count, :integer, default: 0 + if subsecond_precision_supported? + t.datetime :created_at, precision: 0 + t.datetime :created_on, precision: 0 + t.datetime :updated_at, precision: 0 + t.datetime :updated_on, precision: 0 + else + t.datetime :created_at + t.datetime :created_on + t.datetime :updated_at + t.datetime :updated_on + end end - end - create_table :parrots_pirates, id: false, force: true do |t| - t.column :parrot_id, :integer - t.column :pirate_id, :integer - end + create_table :pirates, force: :cascade do |t| + t.string :catchphrase + t.integer :parrot_id + t.integer :non_validated_parrot_id + if subsecond_precision_supported? + t.datetime :created_on, precision: 6 + t.datetime :updated_on, precision: 6 + else + t.datetime :created_on + t.datetime :updated_on + end + end - create_table :parrots_treasures, id: false, force: true do |t| - t.column :parrot_id, :integer - t.column :treasure_id, :integer + create_table :treasures, force: :cascade do |t| + t.string :name + t.string :type + t.references :looter, polymorphic: true + t.references :ship + end + + create_table :parrots_pirates, id: false, force: true do |t| + t.references :parrot, foreign_key: true + t.references :pirate, foreign_key: true + end + + create_table :parrots_treasures, id: false, force: true do |t| + t.references :parrot, foreign_key: true + t.references :treasure, foreign_key: true + end end create_table :people, force: true do |t| @@ -672,19 +695,6 @@ ActiveRecord::Schema.define do t.column :rainbow_color, :string end - create_table :pirates, force: true do |t| - t.column :catchphrase, :string - t.column :parrot_id, :integer - t.integer :non_validated_parrot_id - if subsecond_precision_supported? - t.column :created_on, :datetime, precision: 6 - t.column :updated_on, :datetime, precision: 6 - else - t.column :created_on, :datetime - t.column :updated_on, :datetime - end - end - create_table :posts, force: true do |t| t.references :author t.string :title, null: false @@ -915,14 +925,6 @@ ActiveRecord::Schema.define do t.datetime :updated_at end - create_table :treasures, force: true do |t| - t.column :name, :string - t.column :type, :string - t.column :looter_id, :integer - t.column :looter_type, :string - t.belongs_to :ship - end - create_table :tuning_pegs, force: true do |t| t.integer :guitar_id t.float :pitch @@ -946,7 +948,7 @@ ActiveRecord::Schema.define do end [:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t| - create_table(t, force: true) {} + create_table(t, force: true) { } end create_table :men, force: true do |t| @@ -982,14 +984,16 @@ ActiveRecord::Schema.define do t.references :wheelable, polymorphic: true end - create_table :countries, force: true, id: false, primary_key: "country_id" do |t| - t.string :country_id + create_table :countries, force: true, id: false do |t| + t.string :country_id, primary_key: true t.string :name end - create_table :treaties, force: true, id: false, primary_key: "treaty_id" do |t| - t.string :treaty_id + + create_table :treaties, force: true, id: false do |t| + t.string :treaty_id, primary_key: true t.string :name end + create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t| t.string :country_id, null: false t.string :treaty_id, null: false diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb new file mode 100644 index 0000000000..18192292e4 --- /dev/null +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define do + create_table :defaults, force: true do |t| + t.date :fixed_date, default: "2004-01-01" + t.datetime :fixed_time, default: "2004-01-01 00:00:00" + t.column :char1, "char(1)", default: "Y" + t.string :char2, limit: 50, default: "a varchar field" + t.text :char3, limit: 50, default: "a text field" + end +end diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 0e9c2b5619..f4e2826dc6 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,41 @@ +* Add `ActiveStorage.routes_prefix` for configuring generated routes. + + *Chris Bisnett* + +* `ActiveStorage::Service::AzureStorageService` only handles specifically + relevant types of `Azure::Core::Http::HTTPError`. It previously obscured + other types of `HTTPError`, which is the azure-storage gem’s catch-all + exception class. + + *Cameron Bothner* + +* `ActiveStorage::DiskController#show` generates a 404 Not Found response when + the requested file is missing from the disk service. It previously raised + `Errno::ENOENT`. + + *Cameron Bothner* + +* `ActiveStorage::Blob#download` and `ActiveStorage::Blob#open` raise + `ActiveStorage::FileNotFoundError` when the corresponding file is missing + from the storage service. Services translate service-specific missing object + exceptions (e.g. `Google::Cloud::NotFoundError` for the GCS service and + `Errno::ENOENT` for the disk service) into + `ActiveStorage::FileNotFoundError`. + + *Cameron Bothner* + +* Added the `ActiveStorage::SetCurrent` concern for custom Active Storage + controllers that can't inherit from `ActiveStorage::BaseController`. + + *George Claghorn* + +* Active Storage error classes like `ActiveStorage::IntegrityError` and + `ActiveStorage::UnrepresentableError` now inherit from `ActiveStorage::Error` + instead of `StandardError`. This permits rescuing `ActiveStorage::Error` to + handle all Active Storage errors. + + *Andrei Makarov*, *George Claghorn* + * Uploaded files assigned to a record are persisted to storage when the record is saved instead of immediately. diff --git a/activestorage/README.md b/activestorage/README.md index b677721d95..bd31f0ea58 100644 --- a/activestorage/README.md +++ b/activestorage/README.md @@ -16,6 +16,8 @@ A key difference to how Active Storage works compared to other attachment soluti Run `rails active_storage:install` to copy over active_storage migrations. +NOTE: If the task cannot be found, verify that `require "active_storage/engine"` is present in `config/application.rb`. + ## Examples One attachment: diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec index cb1bb00a25..2c8816df25 100644 --- a/activestorage/activestorage.gemspec +++ b/activestorage/activestorage.gemspec @@ -25,6 +25,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activestorage/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "actionpack", version s.add_dependency "activerecord", version diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index d3fd795a3a..375eb6b533 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -855,14 +855,22 @@ return DirectUploadsController; }(); var processingAttribute = "data-direct-uploads-processing"; + var submitButtonsByForm = new WeakMap(); var started = false; function start() { if (!started) { started = true; + document.addEventListener("click", didClick, true); document.addEventListener("submit", didSubmitForm); document.addEventListener("ajax:before", didSubmitRemoteElement); } } + function didClick(event) { + var target = event.target; + if (target.tagName == "INPUT" && target.type == "submit" && target.form) { + submitButtonsByForm.set(target.form, target); + } + } function didSubmitForm(event) { handleFormSubmissionEvent(event); } @@ -894,7 +902,7 @@ } } function submitForm(form) { - var button = findElement(form, "input[type=submit]"); + var button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit]"); if (button) { var _button = button, disabled = _button.disabled; button.disabled = false; @@ -909,6 +917,7 @@ button.click(); form.removeChild(button); } + submitButtonsByForm.delete(form); } function disable(input) { input.disabled = true; diff --git a/activestorage/app/controllers/active_storage/base_controller.rb b/activestorage/app/controllers/active_storage/base_controller.rb index 59312ac8df..b27d2bd8aa 100644 --- a/activestorage/app/controllers/active_storage/base_controller.rb +++ b/activestorage/app/controllers/active_storage/base_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -# The base controller for all ActiveStorage controllers. +# The base class for all Active Storage controllers. class ActiveStorage::BaseController < ActionController::Base - protect_from_forgery with: :exception + include ActiveStorage::SetCurrent - before_action do - ActiveStorage::Current.host = request.base_url - end + protect_from_forgery with: :exception end diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 75cc11d6ff..99982202dd 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -13,6 +13,8 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController else head :not_found end + rescue Errno::ENOENT + head :not_found end def update @@ -59,6 +61,6 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController end def acceptable_content?(token) - token[:content_type] == request.content_type && token[:content_length] == request.content_length + token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length end end diff --git a/activestorage/app/controllers/concerns/active_storage/set_current.rb b/activestorage/app/controllers/concerns/active_storage/set_current.rb new file mode 100644 index 0000000000..597afe7064 --- /dev/null +++ b/activestorage/app/controllers/concerns/active_storage/set_current.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs. +# Include this concern in custom controllers that call ActiveStorage::Blob#service_url, +# ActiveStorage::Variant#service_url, or ActiveStorage::Preview#service_url so the disk service can +# generate URLs using the same host, protocol, and base path as the current request. +module ActiveStorage::SetCurrent + extend ActiveSupport::Concern + + included do + before_action do + ActiveStorage::Current.host = request.base_url + end + end +end diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js index 08c535470d..f5353389ef 100644 --- a/activestorage/app/javascript/activestorage/ujs.js +++ b/activestorage/app/javascript/activestorage/ujs.js @@ -2,16 +2,25 @@ import { DirectUploadsController } from "./direct_uploads_controller" import { findElement } from "./helpers" const processingAttribute = "data-direct-uploads-processing" +const submitButtonsByForm = new WeakMap let started = false export function start() { if (!started) { started = true + document.addEventListener("click", didClick, true) document.addEventListener("submit", didSubmitForm) document.addEventListener("ajax:before", didSubmitRemoteElement) } } +function didClick(event) { + const { target } = event + if (target.tagName == "INPUT" && target.type == "submit" && target.form) { + submitButtonsByForm.set(target.form, target) + } +} + function didSubmitForm(event) { handleFormSubmissionEvent(event) } @@ -49,7 +58,8 @@ function handleFormSubmissionEvent(event) { } function submitForm(form) { - let button = findElement(form, "input[type=submit]") + let button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit]") + if (button) { const { disabled } = button button.disabled = false @@ -64,6 +74,7 @@ function submitForm(form) { button.click() form.removeChild(button) } + submitButtonsByForm.delete(form) } function disable(input) { diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb index 2a952f9f74..804ee4557a 100644 --- a/activestorage/app/jobs/active_storage/analyze_job.rb +++ b/activestorage/app/jobs/active_storage/analyze_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob + retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer + def perform(blob) blob.analyze end diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 4bdd1c0224..13758d9179 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -3,9 +3,8 @@ require "active_support/core_ext/module/delegation" # Attachments associate records with blobs. Usually that's a one record-many blobs relationship, -# but it is possible to associate many different records with the same blob. If you're doing that, -# you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying -# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though). +# but it is possible to associate many different records with the same blob. A foreign-key constraint +# on the attachments table prevents blobs from being purged if they’re still attached to any records. class ActiveStorage::Attachment < ActiveRecord::Base self.table_name = "active_storage_attachments" diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index e7f2615b0f..53aa9f0237 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -35,6 +35,10 @@ class ActiveStorage::Blob < ActiveRecord::Base scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) } + before_destroy(prepend: true) do + raise ActiveRecord::InvalidForeignKey if attachments.exists? + end + class << self # You can used the signed ID of a blob to refer to it on the client side without fear of tampering. # This is particularly helpful for direct uploads where the client-side needs to refer to the blob diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb index 049e45dc3e..2c17ddc25f 100644 --- a/activestorage/app/models/active_storage/blob/identifiable.rb +++ b/activestorage/app/models/active_storage/blob/identifiable.rb @@ -15,6 +15,10 @@ module ActiveStorage::Blob::Identifiable end def download_identifiable_chunk - service.download_chunk key, 0...4.kilobytes + if byte_size.positive? + service.download_chunk key, 0...4.kilobytes + else + "" + end end end diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb index bebb5e61b3..2a03e0173d 100644 --- a/activestorage/app/models/active_storage/filename.rb +++ b/activestorage/app/models/active_storage/filename.rb @@ -3,8 +3,6 @@ # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization. # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting. class ActiveStorage::Filename - require_dependency "active_storage/filename/parameters" - include Comparable class << self @@ -60,10 +58,6 @@ class ActiveStorage::Filename @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-") end - def parameters #:nodoc: - Parameters.new self - end - # Returns the sanitized version of the filename. def to_s sanitized.to_s diff --git a/activestorage/app/models/active_storage/filename/parameters.rb b/activestorage/app/models/active_storage/filename/parameters.rb deleted file mode 100644 index fb9ea10e49..0000000000 --- a/activestorage/app/models/active_storage/filename/parameters.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class ActiveStorage::Filename::Parameters #:nodoc: - attr_reader :filename - - def initialize(filename) - @filename = filename - end - - def combined - "#{ascii}; #{utf8}" - end - - TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/ - - def ascii - 'filename="' + percent_escape(I18n.transliterate(filename.sanitized), TRADITIONAL_ESCAPED_CHAR) + '"' - end - - RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/ - - def utf8 - "filename*=UTF-8''" + percent_escape(filename.sanitized, RFC_5987_ESCAPED_CHAR) - end - - def to_s - combined - end - - private - def percent_escape(string, pattern) - string.gsub(pattern) do |char| - char.bytes.map { |byte| "%%%02X" % byte }.join - end - end -end diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index 056fd2be99..ea57fa5f78 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "ostruct" + # Image blobs can have variants that are the result of a set of transformations applied to the original. # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the # original. @@ -51,7 +53,7 @@ # * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods] # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image] class ActiveStorage::Variant - WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif ) + WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ] attr_reader :blob, :variation delegate :service, to: :blob @@ -95,37 +97,35 @@ class ActiveStorage::Variant def process blob.open do |image| - transform image do |output| - upload output - end + transform(image) { |output| upload(output) } end end - - def filename - if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) - blob.filename - else - ActiveStorage::Filename.new("#{blob.filename.base}.png") - end + def transform(image, &block) + variation.transform(image, format: format, &block) end - def content_type - blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png" + def upload(file) + service.upload(key, file) end - def transform(image) - format = "png" unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) - result = variation.transform(image, format: format) - begin - yield result - ensure - result.close! - end + def specification + @specification ||= + if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) + Specification.new \ + filename: blob.filename, + content_type: blob.content_type, + format: nil + else + Specification.new \ + filename: ActiveStorage::Filename.new("#{blob.filename.base}.png"), + content_type: "image/png", + format: "png" + end end - def upload(file) - service.upload(key, file) - end + delegate :filename, :content_type, :format, to: :specification + + class Specification < OpenStruct; end end diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index ae1376a6cf..3adc2407e5 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -47,13 +47,9 @@ class ActiveStorage::Variation # saves the transformed image into a temporary file. If +format+ is specified # it will be the format of the result image, otherwise the result image # retains the source format. - def transform(file, format: nil) + def transform(file, format: nil, &block) ActiveSupport::Notifications.instrument("transform.active_storage") do - if processor - image_processing_transform(file, format) - else - mini_magick_transform(file, format) - end + transformer.transform(file, format: format, &block) end end @@ -63,63 +59,22 @@ class ActiveStorage::Variation end private - # Applies image transformations using the ImageProcessing gem. - def image_processing_transform(file, format) - operations = transformations.each_with_object([]) do |(name, argument), list| - if name.to_s == "combine_options" - ActiveSupport::Deprecation.warn("The ImageProcessing ActiveStorage variant backend doesn't need :combine_options, as it already generates a single MiniMagick command. In Rails 6.1 :combine_options will not be supported anymore.") - list.concat argument.keep_if { |key, value| value.present? }.to_a - elsif argument.present? - list << [name, argument] + def transformer + if ActiveStorage.variant_processor + begin + require "image_processing" + rescue LoadError + ActiveSupport::Deprecation.warn <<~WARNING + Generating image variants will require the image_processing gem in Rails 6.1. + Please add `gem 'image_processing', '~> 1.2'` to your Gemfile. + WARNING + + ActiveStorage::Transformers::MiniMagickTransformer.new(transformations) + else + ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations) end - end - - processor - .source(file) - .loader(page: 0) - .convert(format) - .apply(operations) - .call - end - - # Applies image transformations using the MiniMagick gem. - def mini_magick_transform(file, format) - image = MiniMagick::Image.new(file.path, file) - - transformations.each do |name, argument_or_subtransformations| - image.mogrify do |command| - if name.to_s == "combine_options" - argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument| - pass_transform_argument(command, subtransformation_name, subtransformation_argument) - end - else - pass_transform_argument(command, name, argument_or_subtransformations) - end - end - end - - image.format(format) if format - - image.tempfile.tap(&:open) - end - - # Returns the ImageProcessing processor class specified by `ActiveStorage.variant_processor`. - def processor - begin - require "image_processing" - rescue LoadError - ActiveSupport::Deprecation.warn("Using mini_magick gem directly is deprecated and will be removed in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.") - return nil - end - - ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize) if ActiveStorage.variant_processor - end - - def pass_transform_argument(command, method, argument) - if argument == true - command.public_send(method) - elsif argument.present? - command.public_send(method, argument) + else + ActiveStorage::Transformers::MiniMagickTransformer.new(transformations) end end end diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb index 20d19f334a..3af7361cff 100644 --- a/activestorage/config/routes.rb +++ b/activestorage/config/routes.rb @@ -1,17 +1,15 @@ # frozen_string_literal: true Rails.application.routes.draw do - get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob - - direct :rails_blob do |blob, options| - route_for(:rails_service_blob, blob.signed_id, blob.filename, options) - end - - resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) } - resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) } + scope ActiveStorage.routes_prefix do + get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob + get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation - get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation + get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service + put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service + post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads + end direct :rails_representation do |representation, options| signed_blob_id = representation.blob.signed_id @@ -25,7 +23,10 @@ Rails.application.routes.draw do resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) } - get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service - put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service - post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads + direct :rails_blob do |blob, options| + route_for(:rails_service_blob, blob.signed_id, blob.filename, options) + end + + resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) } + resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) } end diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index 928db32691..a94ef626f2 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -50,4 +50,13 @@ module ActiveStorage mattr_accessor :variable_content_types, default: [] mattr_accessor :content_types_to_serve_as_binary, default: [] mattr_accessor :service_urls_expire_in, default: 5.minutes + mattr_accessor :routes_prefix, default: "/rails/active_storage" + + module Transformers + extend ActiveSupport::Autoload + + autoload :Transformer + autoload :ImageProcessingTransformer + autoload :MiniMagickTransformer + end end diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 9d6a27eabe..7eb93b5e16 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -51,6 +51,7 @@ module ActiveStorage ActiveStorage.previewers = app.config.active_storage.previewers || [] ActiveStorage.analyzers = app.config.active_storage.analyzers || [] ActiveStorage.paths = app.config.active_storage.paths || {} + ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage" ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || [] ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || [] diff --git a/activestorage/lib/active_storage/errors.rb b/activestorage/lib/active_storage/errors.rb index bedcd080c4..6475c1d076 100644 --- a/activestorage/lib/active_storage/errors.rb +++ b/activestorage/lib/active_storage/errors.rb @@ -1,11 +1,26 @@ # frozen_string_literal: true module ActiveStorage - class InvariableError < StandardError; end - class UnpreviewableError < StandardError; end - class UnrepresentableError < StandardError; end + # Generic base class for all Active Storage exceptions. + class Error < StandardError; end + + # Raised when ActiveStorage::Blob#variant is called on a blob that isn't variable. + # Use ActiveStorage::Blob#variable? to determine whether a blob is variable. + class InvariableError < Error; end + + # Raised when ActiveStorage::Blob#preview is called on a blob that isn't previewable. + # Use ActiveStorage::Blob#previewable? to determine whether a blob is previewable. + class UnpreviewableError < Error; end + + # Raised when ActiveStorage::Blob#representation is called on a blob that isn't representable. + # Use ActiveStorage::Blob#representable? to determine whether a blob is representable. + class UnrepresentableError < Error; end # Raised when uploaded or downloaded data does not match a precomputed checksum. # Indicates that a network error or a software bug caused data corruption. - class IntegrityError < StandardError; end + class IntegrityError < Error; end + + # Raised when ActiveStorage::Blob#download is called on a blob where the + # backing file is no longer present in its service. + class FileNotFoundError < Error; end end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index f915518f52..54ba08fb87 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require "active_storage/log_subscriber" +require "action_dispatch" +require "action_dispatch/http/content_disposition" module ActiveStorage # Abstract class serving as an interface for concrete services. @@ -122,7 +124,8 @@ module ActiveStorage end def content_disposition_with(type: "inline", filename:) - (type.to_s.presence_in(%w( attachment inline )) || "inline") + "; #{filename.parameters}" + disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline") + ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized) end end end diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index b26234c722..8de3889cb5 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -19,10 +19,8 @@ module ActiveStorage def upload(key, io, checksum: nil) instrument :upload, key: key, checksum: checksum do - begin + handle_errors do blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum) - rescue Azure::Core::Http::HTTPError - raise ActiveStorage::IntegrityError end end end @@ -34,16 +32,20 @@ module ActiveStorage end else instrument :download, key: key do - _, io = blobs.get_blob(container, key) - io.force_encoding(Encoding::BINARY) + handle_errors do + _, io = blobs.get_blob(container, key) + io.force_encoding(Encoding::BINARY) + end end end end def download_chunk(key, range) instrument :download_chunk, key: key, range: range do - _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end) - io.force_encoding(Encoding::BINARY) + handle_errors do + _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end) + io.force_encoding(Encoding::BINARY) + end end end @@ -51,7 +53,8 @@ module ActiveStorage instrument :delete, key: key do begin blobs.delete_blob(container, key) - rescue Azure::Core::Http::HTTPError + rescue Azure::Core::Http::HTTPError => e + raise unless e.type == "BlobNotFound" # Ignore files already deleted end end @@ -139,11 +142,26 @@ module ActiveStorage chunk_size = 5.megabytes offset = 0 + raise ActiveStorage::FileNotFoundError unless blob.present? + while offset < blob.properties[:content_length] _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1) yield chunk.force_encoding(Encoding::BINARY) offset += chunk_size end end + + def handle_errors + yield + rescue Azure::Core::Http::HTTPError => e + case e.type + when "BlobNotFound" + raise ActiveStorage::FileNotFoundError + when "Md5Mismatch" + raise ActiveStorage::IntegrityError + else + raise + end + end end end diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index 9f304b7e01..52f3a3df16 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -22,27 +22,31 @@ module ActiveStorage end end - def download(key) + def download(key, &block) if block_given? instrument :streaming_download, key: key do - File.open(path_for(key), "rb") do |file| - while data = file.read(5.megabytes) - yield data - end - end + stream key, &block end else instrument :download, key: key do - File.binread path_for(key) + begin + File.binread path_for(key) + rescue Errno::ENOENT + raise ActiveStorage::FileNotFoundError + end end end end def download_chunk(key, range) instrument :download_chunk, key: key, range: range do - File.open(path_for(key), "rb") do |file| - file.seek range.begin - file.read range.size + begin + File.open(path_for(key), "rb") do |file| + file.seek range.begin + file.read range.size + end + rescue Errno::ENOENT + raise ActiveStorage::FileNotFoundError end end end @@ -122,6 +126,16 @@ module ActiveStorage end private + def stream(key) + File.open(path_for(key), "rb") do |file| + while data = file.read(5.megabytes) + yield data + end + end + rescue Errno::ENOENT + raise ActiveStorage::FileNotFoundError + end + def folder_for(key) [ key[0..1], key[2..3] ].join("/") end diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index eb46973509..18c0f14cfc 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -34,14 +34,22 @@ module ActiveStorage end else instrument :download, key: key do - file_for(key).download.string + begin + file_for(key).download.string + rescue Google::Cloud::NotFoundError + raise ActiveStorage::FileNotFoundError + end end end end def download_chunk(key, range) instrument :download_chunk, key: key, range: range do - file_for(key).download(range: range).string + begin + file_for(key).download(range: range).string + rescue Google::Cloud::NotFoundError + raise ActiveStorage::FileNotFoundError + end end end @@ -116,6 +124,8 @@ module ActiveStorage chunk_size = 5.megabytes offset = 0 + raise ActiveStorage::FileNotFoundError unless file.present? + while offset < file.size yield file.download(range: offset..(offset + chunk_size - 1)).string offset += chunk_size diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index 0286e7ff21..89a9e54158 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -33,14 +33,22 @@ module ActiveStorage end else instrument :download, key: key do - object_for(key).get.body.string.force_encoding(Encoding::BINARY) + begin + object_for(key).get.body.string.force_encoding(Encoding::BINARY) + rescue Aws::S3::Errors::NoSuchKey + raise ActiveStorage::FileNotFoundError + end end end end def download_chunk(key, range) instrument :download_chunk, key: key, range: range do - object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY) + begin + object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY) + rescue Aws::S3::Errors::NoSuchKey + raise ActiveStorage::FileNotFoundError + end end end @@ -103,6 +111,8 @@ module ActiveStorage chunk_size = 5.megabytes offset = 0 + raise ActiveStorage::FileNotFoundError unless object.exists? + while offset < object.content_length yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY) offset += chunk_size diff --git a/activestorage/lib/active_storage/transformers/image_processing_transformer.rb b/activestorage/lib/active_storage/transformers/image_processing_transformer.rb new file mode 100644 index 0000000000..7f8685b72d --- /dev/null +++ b/activestorage/lib/active_storage/transformers/image_processing_transformer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "image_processing" + +module ActiveStorage + module Transformers + class ImageProcessingTransformer < Transformer + private + def process(file, format:) + processor. + source(file). + loader(page: 0). + convert(format). + apply(operations). + call + end + + def processor + ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize) + end + + def operations + transformations.each_with_object([]) do |(name, argument), list| + if name.to_s == "combine_options" + ActiveSupport::Deprecation.warn <<~WARNING + Active Storage's ImageProcessing transformer doesn't support :combine_options, + as it always generates a single ImageMagick command. Passing :combine_options will + not be supported in Rails 6.1. + WARNING + + list.concat argument.keep_if { |key, value| value.present? }.to_a + elsif argument.present? + list << [ name, argument ] + end + end + end + end + end +end diff --git a/activestorage/lib/active_storage/transformers/mini_magick_transformer.rb b/activestorage/lib/active_storage/transformers/mini_magick_transformer.rb new file mode 100644 index 0000000000..e8e99cea9e --- /dev/null +++ b/activestorage/lib/active_storage/transformers/mini_magick_transformer.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "mini_magick" + +module ActiveStorage + module Transformers + class MiniMagickTransformer < Transformer + private + def process(file, format:) + image = MiniMagick::Image.new(file.path, file) + + transformations.each do |name, argument_or_subtransformations| + image.mogrify do |command| + if name.to_s == "combine_options" + argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument| + pass_transform_argument(command, subtransformation_name, subtransformation_argument) + end + else + pass_transform_argument(command, name, argument_or_subtransformations) + end + end + end + + image.format(format) if format + + image.tempfile.tap(&:open) + end + + def pass_transform_argument(command, method, argument) + if argument == true + command.public_send(method) + elsif argument.present? + command.public_send(method, argument) + end + end + end + end +end diff --git a/activestorage/lib/active_storage/transformers/transformer.rb b/activestorage/lib/active_storage/transformers/transformer.rb new file mode 100644 index 0000000000..2e21201004 --- /dev/null +++ b/activestorage/lib/active_storage/transformers/transformer.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module ActiveStorage + module Transformers + # A Transformer applies a set of transformations to an image. + # + # The following concrete subclasses are included in Active Storage: + # + # * ActiveStorage::Transformers::ImageProcessingTransformer: + # backed by ImageProcessing, a common interface for MiniMagick and ruby-vips + # + # * ActiveStorage::Transformers::MiniMagickTransformer: + # backed by MiniMagick, a wrapper around the ImageMagick CLI + class Transformer + attr_reader :transformations + + def initialize(transformations) + @transformations = transformations + end + + # Applies the transformations to the source image in +file+, producing a target image in the + # specified +format+. Yields an open Tempfile containing the target image. Closes and unlinks + # the output tempfile after yielding to the given block. Returns the result of the block. + def transform(file, format:) + output = process(file, format: format) + + begin + yield output + ensure + output.close! + end + end + + private + # Returns an open Tempfile containing a transformed image in the given +format+. + # All subclasses implement this method. + def process(file, format:) #:doc: + raise NotImplementedError + end + end + end +end diff --git a/activestorage/test/controllers/disk_controller_test.rb b/activestorage/test/controllers/disk_controller_test.rb index c053052f6f..7b5e989699 100644 --- a/activestorage/test/controllers/disk_controller_test.rb +++ b/activestorage/test/controllers/disk_controller_test.rb @@ -31,6 +31,14 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest assert_equal " worl", response.body end + test "showing blob that does not exist" do + blob = create_blob + blob.delete + + get blob.service_url + assert_response :not_found + end + test "directly uploading blob with integrity" do data = "Something else entirely!" @@ -59,6 +67,16 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest assert_not blob.service.exist?(blob.key) end + test "directly uploading blob with different but equivalent content type" do + data = "Something else entirely!" + blob = create_blob_before_direct_upload( + byte_size: data.size, checksum: Digest::MD5.base64digest(data), content_type: "application/x-gzip") + + put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "application/x-gzip" } + assert_response :no_content + assert_equal data, blob.download + end + test "directly uploading blob with mismatched content length" do data = "Something else entirely!" blob = create_blob_before_direct_upload byte_size: data.size - 1, checksum: Digest::MD5.base64digest(data) diff --git a/activestorage/test/dummy/app/assets/config/manifest.js b/activestorage/test/dummy/app/assets/config/manifest.js index a8adebe722..bb109908b2 100644 --- a/activestorage/test/dummy/app/assets/config/manifest.js +++ b/activestorage/test/dummy/app/assets/config/manifest.js @@ -1,5 +1,3 @@ //= link_tree ../images -//= link_directory ../javascripts .js //= link_directory ../stylesheets .css -//= link active_storage_manifest.js diff --git a/activestorage/test/dummy/config/environments/production.rb b/activestorage/test/dummy/config/environments/production.rb index 34ac3bc561..be7f5b80d4 100644 --- a/activestorage/test/dummy/config/environments/production.rb +++ b/activestorage/test/dummy/config/environments/production.rb @@ -25,8 +25,7 @@ Rails.application.configure do # Apache or NGINX already handles this. config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. diff --git a/activestorage/test/dummy/config/webpacker.yml b/activestorage/test/dummy/config/webpacker.yml new file mode 100644 index 0000000000..c1515a2e95 --- /dev/null +++ b/activestorage/test/dummy/config/webpacker.yml @@ -0,0 +1,72 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_output_path: packs + cache_path: tmp/cache/webpacker + check_yarn_integrity: false + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + extensions: + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Verifies that versions and hashed value of the package contents in the project's package.json + check_yarn_integrity: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: /node_modules/ + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Cache manifest.json for performance + cache_manifest: true diff --git a/activestorage/test/fixtures/files/empty_file.txt b/activestorage/test/fixtures/files/empty_file.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activestorage/test/fixtures/files/empty_file.txt diff --git a/activestorage/test/jobs/purge_job_test.rb b/activestorage/test/jobs/purge_job_test.rb index ed4100b78d..251022a96f 100644 --- a/activestorage/test/jobs/purge_job_test.rb +++ b/activestorage/test/jobs/purge_job_test.rb @@ -24,14 +24,4 @@ class ActiveStorage::PurgeJobTest < ActiveJob::TestCase end end end - - test "ignores attached blob" do - User.create! name: "DHH", avatar: @blob - - perform_enqueued_jobs do - assert_nothing_raised do - ActiveStorage::PurgeJob.perform_later @blob - end - end - end end diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 88c106a08b..1a6a89de56 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -185,7 +185,7 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase private def expected_url_for(blob, disposition: :inline, filename: nil) filename ||= blob.filename - query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{filename.parameters}" }.to_param + query_string = { content_type: blob.content_type, disposition: ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized) }.to_param "https://example.com/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}" end end diff --git a/activestorage/test/models/filename/parameters_test.rb b/activestorage/test/models/filename/parameters_test.rb deleted file mode 100644 index 431be00639..0000000000 --- a/activestorage/test/models/filename/parameters_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class ActiveStorage::Filename::ParametersTest < ActiveSupport::TestCase - test "parameterizing a Latin filename" do - filename = ActiveStorage::Filename.new("racecar.jpg") - - assert_equal %(filename="racecar.jpg"), filename.parameters.ascii - assert_equal "filename*=UTF-8''racecar.jpg", filename.parameters.utf8 - assert_equal "#{filename.parameters.ascii}; #{filename.parameters.utf8}", filename.parameters.combined - assert_equal filename.parameters.combined, filename.parameters.to_s - end - - test "parameterizing a Latin filename with accented characters" do - filename = ActiveStorage::Filename.new("råcëçâr.jpg") - - assert_equal %(filename="racecar.jpg"), filename.parameters.ascii - assert_equal "filename*=UTF-8''r%C3%A5c%C3%AB%C3%A7%C3%A2r.jpg", filename.parameters.utf8 - assert_equal "#{filename.parameters.ascii}; #{filename.parameters.utf8}", filename.parameters.combined - assert_equal filename.parameters.combined, filename.parameters.to_s - end - - test "parameterizing a non-Latin filename" do - filename = ActiveStorage::Filename.new("автомобиль.jpg") - - assert_equal %(filename="%3F%3F%3F%3F%3F%3F%3F%3F%3F%3F.jpg"), filename.parameters.ascii - assert_equal "filename*=UTF-8''%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C.jpg", filename.parameters.utf8 - assert_equal "#{filename.parameters.ascii}; #{filename.parameters.utf8}", filename.parameters.combined - assert_equal filename.parameters.combined, filename.parameters.to_s - end -end diff --git a/activestorage/test/models/filename_test.rb b/activestorage/test/models/filename_test.rb index 88405e41c0..715116309f 100644 --- a/activestorage/test/models/filename_test.rb +++ b/activestorage/test/models/filename_test.rb @@ -30,8 +30,8 @@ class ActiveStorage::FilenameTest < ActiveSupport::TestCase end test "sanitize transcodes to valid UTF-8" do - { "\xF6".dup.force_encoding(Encoding::ISO8859_1) => "ö", - "\xC3".dup.force_encoding(Encoding::ISO8859_1) => "Ã", + { (+"\xF6").force_encoding(Encoding::ISO8859_1) => "ö", + (+"\xC3").force_encoding(Encoding::ISO8859_1) => "Ã", "\xAD" => "�", "\xCF" => "�", "\x00" => "", diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb index 4bfcda017f..559aa028f2 100644 --- a/activestorage/test/service/s3_service_test.rb +++ b/activestorage/test/service/s3_service_test.rb @@ -31,6 +31,13 @@ if SERVICE_CONFIGURATIONS[:s3] end end + test "upload a zero byte file" do + blob = directly_upload_file_blob filename: "empty_file.txt", content_type: nil + user = User.create! name: "DHH", avatar: blob + + assert_equal user.avatar.blob, blob + end + test "signed URL generation" do url = @service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png") diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb index 30cfca4e36..ca2490f2bc 100644 --- a/activestorage/test/service/shared_service_tests.rb +++ b/activestorage/test/service/shared_service_tests.rb @@ -6,7 +6,7 @@ require "active_support/core_ext/securerandom" module ActiveStorage::Service::SharedServiceTests extend ActiveSupport::Concern - FIXTURE_DATA = "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\020\000\000\000\020\001\003\000\000\000%=m\"\000\000\000\006PLTE\000\000\000\377\377\377\245\331\237\335\000\000\0003IDATx\234c\370\377\237\341\377_\206\377\237\031\016\2603\334?\314p\1772\303\315\315\f7\215\031\356\024\203\320\275\317\f\367\201R\314\f\017\300\350\377\177\000Q\206\027(\316]\233P\000\000\000\000IEND\256B`\202".dup.force_encoding(Encoding::BINARY) + FIXTURE_DATA = (+"\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\020\000\000\000\020\001\003\000\000\000%=m\"\000\000\000\006PLTE\000\000\000\377\377\377\245\331\237\335\000\000\0003IDATx\234c\370\377\237\341\377_\206\377\237\031\016\2603\334?\314p\1772\303\315\315\f7\215\031\356\024\203\320\275\317\f\367\201R\314\f\017\300\350\377\177\000Q\206\027(\316]\233P\000\000\000\000IEND\256B`\202").force_encoding(Encoding::BINARY) included do setup do @@ -50,6 +50,13 @@ module ActiveStorage::Service::SharedServiceTests assert_equal FIXTURE_DATA, @service.download(@key) end + test "downloading a nonexistent file" do + assert_raises(ActiveStorage::FileNotFoundError) do + @service.download(SecureRandom.base58(24)) + end + end + + test "downloading in chunks" do key = SecureRandom.base58(24) expected_chunks = [ "a" * 5.megabytes, "b" ] @@ -68,11 +75,25 @@ module ActiveStorage::Service::SharedServiceTests end end + test "downloading a nonexistent file in chunks" do + assert_raises(ActiveStorage::FileNotFoundError) do + @service.download(SecureRandom.base58(24)) { } + end + end + + test "downloading partially" do assert_equal "\x10\x00\x00", @service.download_chunk(@key, 19..21) assert_equal "\x10\x00\x00", @service.download_chunk(@key, 19...22) end + test "partially downloading a nonexistent file" do + assert_raises(ActiveStorage::FileNotFoundError) do + @service.download_chunk(SecureRandom.base58(24), 19..21) + end + end + + test "existing" do assert @service.exist?(@key) assert_not @service.exist?(@key + "nonsense") diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 7b7926ac79..144c224421 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -18,8 +18,7 @@ require "active_job" ActiveJob::Base.queue_adapter = :test ActiveJob::Base.logger = ActiveSupport::Logger.new(nil) -# Filter out Minitest backtrace while allowing backtrace from other libraries -# to be shown. +# Filter out the backtrace from minitest while preserving the one from other libraries. Minitest.backtrace_filter = Minitest::BacktraceFilter.new require "yaml" diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 5423d59984..98d232da33 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,105 @@ +* Fix bug where `#to_options` for `ActiveSupport::HashWithIndifferentAccess` + would not act as alias for `#symbolize_keys`. + + *Nick Weiland* + +* Improve the logic that detects non-autoloaded constants. + + *Jan Habermann*, *Xavier Noria* + +* Deprecate `ActiveSupport::Multibyte::Unicode#pack_graphemes(array)` and `ActiveSuppport::Multibyte::Unicode#unpack_graphemes(string)` + in favor of `array.flatten.pack("U*")` and `string.scan(/\X/).map(&:codepoints)`, respectively. + + *Francesco Rodríguez* + +* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`. + + *Francesco Rodríguez* + +* Fix duration being rounded to a full second. + ``` + time = DateTime.parse("2018-1-1") + time += 0.51.seconds + ``` + Will now correctly add 0.51 second and not 1 full second. + + *Edouard Chin* + +* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize` + in favor of `String#unicode_normalize` + + *Francesco Rodríguez* + +* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of + `String#downcase/upcase/swapcase`. + + *Francesco Rodríguez* + +* Add `ActiveSupport::ParameterFilter`. + + *Yoshiyuki Kinjo* + +* Rename `Module#parent`, `Module#parents`, and `Module#parent_name` to + `module_parent`, `module_parents`, and `module_parent_name`. + + *Gannon McGibbon* + +* Deprecate the use of `LoggerSilence` in favor of `ActiveSupport::LoggerSilence` + + *Edouard Chin* + +* Deprecate using negative limits in `String#first` and `String#last`. + + *Gannon McGibbon*, *Eric Turner* + +* Fix bug where `#without` for `ActiveSupport::HashWithIndifferentAccess` would fail + with symbol arguments + + *Abraham Chan* + +* Treat `#delete_prefix`, `#delete_suffix` and `#unicode_normalize` results as non-`html_safe`. + Ensure safety of arguments for `#insert`, `#[]=` and `#replace` calls on `html_safe` Strings. + + *Janosch Müller* + +* Changed `ActiveSupport::TaggedLogging.new` to return a new logger instance instead + of mutating the one received as parameter. + + *Thierry Joyal* + +* Define `unfreeze_time` as an alias of `travel_back` in `ActiveSupport::Testing::TimeHelpers`. + + The alias is provided for symmetry with `freeze_time`. + + *Ryan Davidson* + +* Add support for tracing constant autoloads. Just throw + + ActiveSupport::Dependencies.logger = Rails.logger + ActiveSupport::Dependencies.verbose = true + + in an initializer. + + *Xavier Noria* + +* Maintain `html_safe?` on html_safe strings when sliced. + + string = "<div>test</div>".html_safe + string[-1..1].html_safe? # => true + + *Elom Gomez*, *Yumin Wong* + +* Add `Array#extract!`. + + The method removes and returns the elements for which the block returns a true value. + If no block is given, an Enumerator is returned instead. + + numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9] + numbers # => [0, 2, 4, 6, 8] + + *bogdanvlviv* + * Support not to cache `nil` for `ActiveSupport::Cache#fetch`. cache.fetch('bar', skip_nil: true) { nil } @@ -39,7 +141,7 @@ *Aaron "t.lo" Patterson* -* Add cpu_time, idle_time, and allocations to Event +* Add cpu_time, idle_time, and allocations to Event. *Eileen M. Uchitelle*, *Aaron Patterson* @@ -53,7 +155,7 @@ *Jason Lee* -* Allow Range#=== and Range#cover? on Range +* Allow `Range#===` and `Range#cover?` on Range. `Range#cover?` can now accept a range argument like `Range#include?` and `Range#===`. `Range#===` works correctly on Ruby 2.6. `Range#include?` is moved @@ -96,7 +198,7 @@ *Godfrey Chan* -* Fix bug where `URI.unscape` would fail with mixed Unicode/escaped character input: +* Fix bug where `URI.unescape` would fail with mixed Unicode/escaped character input: URI.unescape("\xe3\x83\x90") # => "バ" URI.unescape("%E3%83%90") # => "バ" diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index aa695c98b2..448a2eeebb 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -27,6 +27,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "i18n", ">= 0.7", "< 2" s.add_dependency "tzinfo", "~> 1.1" s.add_dependency "minitest", "~> 5.1" diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 16dd733ddb..62973eca58 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -31,6 +31,9 @@ module ActiveSupport class BacktraceCleaner def initialize @filters, @silencers = [], [] + add_gem_filter + add_gem_silencer + add_stdlib_silencer end # Returns the backtrace after all filters and silencers have been run @@ -82,6 +85,26 @@ module ActiveSupport end private + + FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) / + + def add_gem_filter + gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) } + return if gems_paths.empty? + + gems_regexp = %r{(#{gems_paths.join('|')})/(bundler/)?gems/([^/]+)-([\w.]+)/(.*)} + gems_result = '\3 (\4) \5' + add_filter { |line| line.sub(gems_regexp, gems_result) } + end + + def add_gem_silencer + add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) } + end + + def add_stdlib_silencer + add_silencer { |line| line.start_with?(RbConfig::CONFIG["rubylibdir"]) } + end + def filter_backtrace(backtrace) @filters.each do |f| backtrace = backtrace.map { |line| f.call(line) } diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 8e516de4c9..e8518645d9 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -411,8 +411,6 @@ module ActiveSupport # to the cache. If you do not want to write the cache when the cache is # not found, use #read_multi. # - # Options are passed to the underlying cache implementation. - # # Returns a hash with the data for each of the names. For example: # # cache.write("bim", "bam") @@ -422,6 +420,17 @@ module ActiveSupport # # => { "bim" => "bam", # # "unknown_key" => "Fallback value for key: unknown_key" } # + # Options are passed to the underlying cache implementation. For example: + # + # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key| + # "buzz" + # end + # # => {"fizz"=>"buzz"} + # cache.read("fizz") + # # => "buzz" + # sleep(6) + # cache.read("fizz") + # # => nil def fetch_multi(*names) raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given? @@ -596,9 +605,13 @@ module ActiveSupport # Merges the default options with ones specific to a method call. def merged_options(call_options) if call_options - options.merge(call_options) + if options.empty? + call_options + else + options.merge(call_options) + end else - options.dup + options end end @@ -643,7 +656,7 @@ module ActiveSupport if key.size > 1 key = key.collect { |element| expanded_key(element) } else - key = key.first + key = expanded_key(key.first) end when Hash key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" } @@ -694,7 +707,7 @@ module ActiveSupport end def get_entry_value(entry, name, options) - instrument(:fetch_hit, name, options) {} + instrument(:fetch_hit, name, options) { } entry.value end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index a0f44aac0f..53a2b07536 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -26,6 +26,11 @@ module ActiveSupport @cache_path = cache_path.to_s end + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + # Deletes all items from the cache. In this case it deletes all the entries in the specified # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. @@ -127,15 +132,19 @@ module ActiveSupport hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) - fname_paths = [] # Make sure file name doesn't exceed file system limits. - begin - fname_paths << fname[0, FILENAME_MAX_SIZE] - fname = fname[FILENAME_MAX_SIZE..-1] - end until fname.blank? + if fname.length < FILENAME_MAX_SIZE + fname_paths = fname + else + fname_paths = [] + begin + fname_paths << fname[0, FILENAME_MAX_SIZE] + fname = fname[FILENAME_MAX_SIZE..-1] + end until fname.blank? + end - File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) + File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths) end # Translate a file path into a key. diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 2840781dde..174c784deb 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -47,6 +47,11 @@ module ActiveSupport end end + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + prepend Strategy::LocalCache prepend LocalCacheWithRaw diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 564ac17241..106b616529 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -30,6 +30,11 @@ module ActiveSupport @pruning = false end + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + # Delete all data stored in a given cache store. def clear(options = nil) synchronize do diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb index 1a5983db43..8452a28fd8 100644 --- a/activesupport/lib/active_support/cache/null_store.rb +++ b/activesupport/lib/active_support/cache/null_store.rb @@ -12,6 +12,11 @@ module ActiveSupport class NullStore < Store prepend Strategy::LocalCache + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + def clear(options = nil) end diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 5737450b4a..9a55e49e27 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -66,6 +66,11 @@ module ActiveSupport SCAN_BATCH_SIZE = 1000 private_constant :SCAN_BATCH_SIZE + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + # Support raw values in the local cache strategy. module LocalCacheWithRaw # :nodoc: private diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index c266b432c0..d0644a0f7e 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -23,6 +23,9 @@ module ActiveSupport # +ClassMethods.set_callback+), and run the installed callbacks at the # appropriate times (via +run_callbacks+). # + # By default callbacks are halted by throwing +:abort+. + # See +ClassMethods.define_callbacks+ for details. + # # Three kinds of callbacks are supported: before callbacks, run before a # certain event; after callbacks, run after the event; and around callbacks, # blocks that surround the event, triggering it when they yield. Callback code @@ -657,9 +660,17 @@ module ActiveSupport # * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance # method or a proc; the callback will be called only when they all return # a true value. + # + # If a proc is given, its body is evaluated in the context of the + # current object. It can also optionally accept the current object as + # an argument. # * <tt>:unless</tt> - A symbol or an array of symbols, each naming an # instance method or a proc; the callback will be called only when they # all return a false value. + # + # If a proc is given, its body is evaluated in the context of the + # current object. It can also optionally accept the current object as + # an argument. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index 2610114d8f..9acf674c40 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -2,7 +2,6 @@ require "active_support/concern" require "active_support/ordered_options" -require "active_support/core_ext/array/extract_options" module ActiveSupport # Configurable provides a <tt>config</tt> method to store and retrieve @@ -105,9 +104,7 @@ module ActiveSupport # end # # User.hair_colors # => [:brown, :black, :blonde, :red] - def config_accessor(*names) - options = names.extract_options! - + def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc: names.each do |name| raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name) @@ -117,9 +114,9 @@ module ActiveSupport singleton_class.class_eval reader, __FILE__, reader_line singleton_class.class_eval writer, __FILE__, writer_line - unless options[:instance_accessor] == false - class_eval reader, __FILE__, reader_line unless options[:instance_reader] == false - class_eval writer, __FILE__, writer_line unless options[:instance_writer] == false + if instance_accessor + class_eval reader, __FILE__, reader_line if instance_reader + class_eval writer, __FILE__, writer_line if instance_writer end send("#{name}=", yield) if block_given? end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 6d83b76882..a2569c798b 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -3,6 +3,7 @@ require "active_support/core_ext/array/wrap" require "active_support/core_ext/array/access" require "active_support/core_ext/array/conversions" +require "active_support/core_ext/array/extract" require "active_support/core_ext/array/extract_options" require "active_support/core_ext/array/grouping" require "active_support/core_ext/array/prepend_and_append" diff --git a/activesupport/lib/active_support/core_ext/array/extract.rb b/activesupport/lib/active_support/core_ext/array/extract.rb new file mode 100644 index 0000000000..cc5a8a3f88 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/extract.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Array + # Removes and returns the elements for which the block returns a true value. + # If no block is given, an Enumerator is returned instead. + # + # numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9] + # numbers # => [0, 2, 4, 6, 8] + def extract! + return to_enum(:extract!) { size } unless block_given? + + extracted_elements = [] + + reject! do |element| + extracted_elements << element if yield(element) + end + + extracted_elements + end +end diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 75e65337b7..56fb46a88d 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -3,7 +3,7 @@ class Class begin # Test if this Ruby supports each_object against singleton_class - ObjectSpace.each_object(Numeric.singleton_class) {} + ObjectSpace.each_object(Numeric.singleton_class) { } # Returns an array with all classes that are < than its receiver. # diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index e61b23f842..bc670c3e76 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -110,7 +110,7 @@ class DateTime # instance time. Do not use this method in combination with x.months, use # months_since instead! def since(seconds) - self + Rational(seconds.round, 86400) + self + Rational(seconds, 86400) end alias :in :since diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb index e7606662d3..bd57a909c5 100644 --- a/activesupport/lib/active_support/core_ext/integer/multiple.rb +++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb @@ -7,6 +7,6 @@ class Integer # 6.multiple_of?(5) # => false # 10.multiple_of?(2) # => true def multiple_of?(number) - number != 0 ? self % number == 0 : zero? + number == 0 ? self == 0 : self % number == 0 end end diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index 6b0dcab905..b81ed0605e 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -4,6 +4,6 @@ class LoadError # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) - location.sub(/\.rb$/, "".freeze) == path.to_s.sub(/\.rb$/, "".freeze) + location.sub(/\.rb$/, "") == path.to_s.sub(/\.rb$/, "") end end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 281eaa7d5f..5850e0193f 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/array/extract_options" - # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance # attributes. diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb index b1788a000b..cb996e9e6d 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/array/extract_options" - # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance # attributes, but does so on a per-thread basis. @@ -35,9 +33,7 @@ class Module # end # # Current.new.user # => NoMethodError - def thread_mattr_reader(*syms) # :nodoc: - options = syms.extract_options! - + def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true) # :nodoc: syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) @@ -49,7 +45,7 @@ class Module end EOS - unless options[:instance_reader] == false || options[:instance_accessor] == false + if instance_reader && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} self.class.#{sym} @@ -78,8 +74,7 @@ class Module # end # # Current.new.user = "DHH" # => NoMethodError - def thread_mattr_writer(*syms) # :nodoc: - options = syms.extract_options! + def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc: syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) @@ -91,7 +86,7 @@ class Module end EOS - unless options[:instance_writer] == false || options[:instance_accessor] == false + if instance_writer && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym}=(obj) self.class.#{sym} = obj @@ -141,9 +136,9 @@ class Module # # Current.new.user = "DHH" # => NoMethodError # Current.new.user # => NoMethodError - def thread_mattr_accessor(*syms) - thread_mattr_reader(*syms) - thread_mattr_writer(*syms) + def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true) + thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor) + thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor) end alias :thread_cattr_accessor :thread_mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index c5bb598bd1..9b6df40596 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -5,8 +5,8 @@ require "active_support/inflector" class Module # Returns the name of the module containing this one. # - # M::N.parent_name # => "M" - def parent_name + # M::N.module_parent_name # => "M" + def module_parent_name if defined?(@parent_name) @parent_name else @@ -16,6 +16,14 @@ class Module end end + def parent_name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `Module#parent_name` has been renamed to `module_parent_name`. + `parent_name` is deprecated and will be removed in Rails 6.1. + MSG + module_parent_name + end + # Returns the module which contains this one according to its name. # # module M @@ -24,15 +32,23 @@ class Module # end # X = M::N # - # M::N.parent # => M - # X.parent # => M + # M::N.module_parent # => M + # X.module_parent # => M # # The parent of top-level and anonymous modules is Object. # - # M.parent # => Object - # Module.new.parent # => Object + # M.module_parent # => Object + # Module.new.module_parent # => Object + def module_parent + module_parent_name ? ActiveSupport::Inflector.constantize(module_parent_name) : Object + end + def parent - parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `Module#parent` has been renamed to `module_parent`. + `parent` is deprecated and will be removed in Rails 6.1. + MSG + module_parent end # Returns all the parents of this module according to its name, ordered from @@ -44,13 +60,13 @@ class Module # end # X = M::N # - # M.parents # => [Object] - # M::N.parents # => [M, Object] - # X.parents # => [M, Object] - def parents + # M.module_parents # => [Object] + # M::N.module_parents # => [M, Object] + # X.module_parents # => [M, Object] + def module_parents parents = [] - if parent_name - parts = parent_name.split("::") + if module_parent_name + parts = module_parent_name.split("::") until parts.empty? parents << ActiveSupport::Inflector.constantize(parts * "::") parts.pop @@ -59,4 +75,12 @@ class Module parents << Object unless parents.include? Object parents end + + def parents + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `Module#parents` has been renamed to `module_parents`. + `parents` is deprecated and will be removed in Rails 6.1. + MSG + module_parents + end end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 7fcd0d0311..8acad6164a 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -4,127 +4,129 @@ require "active_support/core_ext/big_decimal/conversions" require "active_support/number_helper" require "active_support/core_ext/module/deprecation" -module ActiveSupport::NumericWithFormat - # Provides options for converting numbers into formatted strings. - # Options are provided for phone numbers, currency, percentage, - # precision, positional notation, file size and pretty printing. - # - # ==== Options - # - # For details on which formats use which options, see ActiveSupport::NumberHelper - # - # ==== Examples - # - # Phone Numbers: - # 5551234.to_s(:phone) # => "555-1234" - # 1235551234.to_s(:phone) # => "123-555-1234" - # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" - # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" - # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" - # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" - # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') - # # => "+1.123.555.1234 x 1343" - # - # Currency: - # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" - # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" - # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" - # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" - # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') - # # => "($1,234,567,890.50)" - # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') - # # => "£1234567890,50" - # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') - # # => "1234567890,50 £" - # - # Percentage: - # 100.to_s(:percentage) # => "100.000%" - # 100.to_s(:percentage, precision: 0) # => "100%" - # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" - # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" - # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" - # 100.to_s(:percentage, format: '%n %') # => "100.000 %" - # - # Delimited: - # 12345678.to_s(:delimited) # => "12,345,678" - # 12345678.05.to_s(:delimited) # => "12,345,678.05" - # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" - # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" - # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" - # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" - # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') - # # => "98 765 432,98" - # - # Rounded: - # 111.2345.to_s(:rounded) # => "111.235" - # 111.2345.to_s(:rounded, precision: 2) # => "111.23" - # 13.to_s(:rounded, precision: 5) # => "13.00000" - # 389.32314.to_s(:rounded, precision: 0) # => "389" - # 111.2345.to_s(:rounded, significant: true) # => "111" - # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" - # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" - # 111.234.to_s(:rounded, locale: :fr) # => "111,234" - # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) - # # => "13" - # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" - # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') - # # => "1.111,23" - # - # Human-friendly size in Bytes: - # 123.to_s(:human_size) # => "123 Bytes" - # 1234.to_s(:human_size) # => "1.21 KB" - # 12345.to_s(:human_size) # => "12.1 KB" - # 1234567.to_s(:human_size) # => "1.18 MB" - # 1234567890.to_s(:human_size) # => "1.15 GB" - # 1234567890123.to_s(:human_size) # => "1.12 TB" - # 1234567890123456.to_s(:human_size) # => "1.1 PB" - # 1234567890123456789.to_s(:human_size) # => "1.07 EB" - # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" - # 483989.to_s(:human_size, precision: 2) # => "470 KB" - # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" - # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" - # 524288000.to_s(:human_size, precision: 5) # => "500 MB" - # - # Human-friendly format: - # 123.to_s(:human) # => "123" - # 1234.to_s(:human) # => "1.23 Thousand" - # 12345.to_s(:human) # => "12.3 Thousand" - # 1234567.to_s(:human) # => "1.23 Million" - # 1234567890.to_s(:human) # => "1.23 Billion" - # 1234567890123.to_s(:human) # => "1.23 Trillion" - # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" - # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" - # 489939.to_s(:human, precision: 2) # => "490 Thousand" - # 489939.to_s(:human, precision: 4) # => "489.9 Thousand" - # 1234567.to_s(:human, precision: 4, - # significant: false) # => "1.2346 Million" - # 1234567.to_s(:human, precision: 1, - # separator: ',', - # significant: false) # => "1,2 Million" - def to_s(format = nil, options = nil) - case format - when nil - super() - when Integer, String - super(format) - when :phone - ActiveSupport::NumberHelper.number_to_phone(self, options || {}) - when :currency - ActiveSupport::NumberHelper.number_to_currency(self, options || {}) - when :percentage - ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) - when :delimited - ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) - when :rounded - ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) - when :human - ActiveSupport::NumberHelper.number_to_human(self, options || {}) - when :human_size - ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) - when Symbol - super() - else - super(format) +module ActiveSupport + module NumericWithFormat + # Provides options for converting numbers into formatted strings. + # Options are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # ==== Options + # + # For details on which formats use which options, see ActiveSupport::NumberHelper + # + # ==== Examples + # + # Phone Numbers: + # 5551234.to_s(:phone) # => "555-1234" + # 1235551234.to_s(:phone) # => "123-555-1234" + # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" + # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" + # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" + # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" + # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') + # # => "+1.123.555.1234 x 1343" + # + # Currency: + # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" + # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" + # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" + # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" + # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') + # # => "($1,234,567,890.50)" + # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') + # # => "£1234567890,50" + # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') + # # => "1234567890,50 £" + # + # Percentage: + # 100.to_s(:percentage) # => "100.000%" + # 100.to_s(:percentage, precision: 0) # => "100%" + # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" + # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" + # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" + # 100.to_s(:percentage, format: '%n %') # => "100.000 %" + # + # Delimited: + # 12345678.to_s(:delimited) # => "12,345,678" + # 12345678.05.to_s(:delimited) # => "12,345,678.05" + # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" + # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" + # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" + # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" + # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') + # # => "98 765 432,98" + # + # Rounded: + # 111.2345.to_s(:rounded) # => "111.235" + # 111.2345.to_s(:rounded, precision: 2) # => "111.23" + # 13.to_s(:rounded, precision: 5) # => "13.00000" + # 389.32314.to_s(:rounded, precision: 0) # => "389" + # 111.2345.to_s(:rounded, significant: true) # => "111" + # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" + # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" + # 111.234.to_s(:rounded, locale: :fr) # => "111,234" + # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) + # # => "13" + # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" + # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') + # # => "1.111,23" + # + # Human-friendly size in Bytes: + # 123.to_s(:human_size) # => "123 Bytes" + # 1234.to_s(:human_size) # => "1.21 KB" + # 12345.to_s(:human_size) # => "12.1 KB" + # 1234567.to_s(:human_size) # => "1.18 MB" + # 1234567890.to_s(:human_size) # => "1.15 GB" + # 1234567890123.to_s(:human_size) # => "1.12 TB" + # 1234567890123456.to_s(:human_size) # => "1.1 PB" + # 1234567890123456789.to_s(:human_size) # => "1.07 EB" + # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" + # 483989.to_s(:human_size, precision: 2) # => "470 KB" + # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" + # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" + # 524288000.to_s(:human_size, precision: 5) # => "500 MB" + # + # Human-friendly format: + # 123.to_s(:human) # => "123" + # 1234.to_s(:human) # => "1.23 Thousand" + # 12345.to_s(:human) # => "12.3 Thousand" + # 1234567.to_s(:human) # => "1.23 Million" + # 1234567890.to_s(:human) # => "1.23 Billion" + # 1234567890123.to_s(:human) # => "1.23 Trillion" + # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" + # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" + # 489939.to_s(:human, precision: 2) # => "490 Thousand" + # 489939.to_s(:human, precision: 4) # => "489.9 Thousand" + # 1234567.to_s(:human, precision: 4, + # significant: false) # => "1.2346 Million" + # 1234567.to_s(:human, precision: 1, + # separator: ',', + # significant: false) # => "1,2 Million" + def to_s(format = nil, options = nil) + case format + when nil + super() + when Integer, String + super(format) + when :phone + ActiveSupport::NumberHelper.number_to_phone(self, options || {}) + when :currency + ActiveSupport::NumberHelper.number_to_currency(self, options || {}) + when :percentage + ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) + when :delimited + ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) + when :rounded + ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) + when :human + ActiveSupport::NumberHelper.number_to_human(self, options || {}) + when :human_size + ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) + when Symbol + super() + else + super(format) + end end end end diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index ad6a9c6146..f36fef6cc9 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -4,7 +4,7 @@ require "concurrent/map" class Object # An object is blank if it's false, empty, or a whitespace string. - # For example, +false+, '', ' ', +nil+, [], and {} are all blank. + # For example, +nil+, '', ' ', [], {}, and +false+ are all blank. # # This simplifies # diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index c874691629..ef8a1f476d 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -4,19 +4,27 @@ require "delegate" module ActiveSupport module Tryable #:nodoc: - def try(*a, &b) - try!(*a, &b) if a.empty? || respond_to?(a.first) + def try(method_name = nil, *args, &b) + if method_name.nil? && block_given? + if b.arity == 0 + instance_eval(&b) + else + yield self + end + elsif respond_to?(method_name) + public_send(method_name, *args, &b) + end end - def try!(*a, &b) - if a.empty? && block_given? + def try!(method_name = nil, *args, &b) + if method_name.nil? && block_given? if b.arity == 0 instance_eval(&b) else yield self end else - public_send(*a, &b) + public_send(method_name, *args, &b) end end end @@ -135,14 +143,14 @@ class NilClass # # With +try+ # @person.try(:children).try(:first).try(:name) - def try(*args) + def try(method_name = nil, *args) nil end # Calling +try!+ on +nil+ always returns +nil+. # # nil.try!(:name) # => nil - def try!(*args) + def try!(method_name = nil, *args) nil end end diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index 8832fbcb3c..024e32db40 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -1,39 +1,41 @@ # frozen_string_literal: true -module ActiveSupport::RangeWithFormat - RANGE_FORMATS = { - db: -> (start, stop) do - case start - when String then "BETWEEN '#{start}' AND '#{stop}'" +module ActiveSupport + module RangeWithFormat + RANGE_FORMATS = { + db: -> (start, stop) do + case start + when String then "BETWEEN '#{start}' AND '#{stop}'" + else + "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" + end + end + } + + # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. + # + # range = (1..100) # => 1..100 + # + # range.to_s # => "1..100" + # range.to_s(:db) # => "BETWEEN '1' AND '100'" + # + # == Adding your own range formats to to_s + # You can add your own formats to the Range::RANGE_FORMATS hash. + # Use the format name as the hash key and a Proc instance. + # + # # config/initializers/range_formats.rb + # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } + def to_s(format = :default) + if formatter = RANGE_FORMATS[format] + formatter.call(first, last) else - "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" + super() end end - } - # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. - # - # range = (1..100) # => 1..100 - # - # range.to_s # => "1..100" - # range.to_s(:db) # => "BETWEEN '1' AND '100'" - # - # == Adding your own range formats to to_s - # You can add your own formats to the Range::RANGE_FORMATS hash. - # Use the format name as the hash key and a Proc instance. - # - # # config/initializers/range_formats.rb - # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } - def to_s(format = :default) - if formatter = RANGE_FORMATS[format] - formatter.call(first, last) - else - super() - end + alias_method :to_default_s, :to_s + alias_method :to_formatted_s, :to_s end - - alias_method :to_default_s, :to_s - alias_method :to_formatted_s, :to_s end Range.prepend(ActiveSupport::RangeWithFormat) diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 58591bbaaf..4ca24028b0 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -75,6 +75,10 @@ class String # str.first(0) # => "" # str.first(6) # => "hello" def first(limit = 1) + ActiveSupport::Deprecation.warn( + "Calling String#first with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + ) if limit < 0 if limit == 0 "" elsif limit >= size @@ -95,6 +99,10 @@ class String # str.last(0) # => "" # str.last(6) # => "hello" def last(limit = 1) + ActiveSupport::Deprecation.warn( + "Calling String#last with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + ) if limit < 0 if limit == 0 "" elsif limit >= size diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index f3bdc2977e..3a80de4617 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -134,8 +134,9 @@ end module ActiveSupport #:nodoc: class SafeBuffer < String UNSAFE_STRING_METHODS = %w( - capitalize chomp chop delete downcase gsub lstrip next reverse rstrip - slice squeeze strip sub succ swapcase tr tr_s upcase + capitalize chomp chop delete delete_prefix delete_suffix + downcase gsub lstrip next reverse rstrip slice squeeze strip + sub succ swapcase tr tr_s unicode_normalize upcase ) alias_method :original_concat, :concat @@ -149,9 +150,7 @@ module ActiveSupport #:nodoc: end def [](*args) - if args.size < 2 - super - elsif html_safe? + if html_safe? new_safe_buffer = super if new_safe_buffer @@ -188,10 +187,22 @@ module ActiveSupport #:nodoc: end alias << concat + def insert(index, value) + super(index, html_escape_interpolated_argument(value)) + end + def prepend(value) super(html_escape_interpolated_argument(value)) end + def replace(value) + super(html_escape_interpolated_argument(value)) + end + + def []=(index, value) + super(index, html_escape_interpolated_argument(value)) + end + def +(other) dup.concat(other) end diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb index 6f9834bb16..60e9952ee6 100644 --- a/activesupport/lib/active_support/core_ext/string/strip.rb +++ b/activesupport/lib/active_support/core_ext/string/strip.rb @@ -20,7 +20,7 @@ class String # Technically, it looks for the least indented non-empty line # in the whole string, and removes that amount of leading whitespace. def strip_heredoc - gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze).tap do |stripped| + gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "").tap do |stripped| stripped.freeze if frozen? end end diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 4e6d8e4585..3145ff87a1 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/callbacks" + module ActiveSupport # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically # before and after each request. This allows you to keep all the per-request attributes easily diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 9dc2c46880..d5d00b5e6e 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -79,6 +79,12 @@ module ActiveSupport #:nodoc: # to allow arbitrary constants to be marked for unloading. mattr_accessor :explicitly_unloadable_constants, default: [] + # The logger used when tracing autoloads. + mattr_accessor :logger + + # If true, trace autoloads with +logger.debug+. + mattr_accessor :verbose, default: false + # The WatchStack keeps a stack of the modules being watched as files are # loaded. If a file in the process of being loaded (parent.rb) triggers the # load of another file (child.rb) the stack will ensure that child.rb @@ -97,6 +103,8 @@ module ActiveSupport #:nodoc: # parent.rb then requires namespace/child.rb, the stack will look like # [[Object], [Namespace]]. + attr_reader :watching + def initialize @watching = [] @stack = Hash.new { |h, k| h[k] = [] } @@ -138,7 +146,7 @@ module ActiveSupport #:nodoc: # Normalize the list of new constants, and add them to the list we will return new_constants.each do |suffix| - constants << ([namespace, suffix] - ["Object"]).join("::".freeze) + constants << ([namespace, suffix] - ["Object"]).join("::") end end constants @@ -248,7 +256,9 @@ module ActiveSupport #:nodoc: def load_dependency(file) if Dependencies.load? && Dependencies.constant_watch_stack.watching? - Dependencies.new_constants_in(Object) { yield } + descs = Dependencies.constant_watch_stack.watching.flatten.uniq + + Dependencies.new_constants_in(*descs) { yield } else yield end @@ -404,7 +414,7 @@ module ActiveSupport #:nodoc: next unless expanded_path.start_with?(expanded_root) root_size = expanded_root.size - next if expanded_path[root_size] != ?/.freeze + next if expanded_path[root_size] != ?/ nesting = expanded_path[(root_size + 1)..-1] paths << nesting.camelize unless nesting.blank? @@ -416,7 +426,7 @@ module ActiveSupport #:nodoc: # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) - path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze) + path_suffix += ".rb" unless path_suffix.ends_with?(".rb") autoload_paths.each do |root| path = File.join(root, path_suffix) @@ -450,6 +460,7 @@ module ActiveSupport #:nodoc: return nil unless base_path = autoloadable_module?(path_suffix) mod = Module.new into.const_set const_name, mod + log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})") autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path) autoloaded_constants.uniq! mod @@ -491,26 +502,31 @@ module ActiveSupport #:nodoc: raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!" end - qualified_name = qualified_name_for from_mod, const_name + qualified_name = qualified_name_for(from_mod, const_name) path_suffix = qualified_name.underscore file_path = search_for_file(path_suffix) if file_path expanded = File.expand_path(file_path) - expanded.sub!(/\.rb\z/, "".freeze) + expanded.sub!(/\.rb\z/, "") if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" else require_or_load(expanded, qualified_name) - raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false) - return from_mod.const_get(const_name) + + if from_mod.const_defined?(const_name, false) + log("constant #{qualified_name} autoloaded from #{expanded}.rb") + return from_mod.const_get(const_name) + else + raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" + end end elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix) return mod - elsif (parent = from_mod.parent) && parent != from_mod && - ! from_mod.parents.any? { |p| p.const_defined?(const_name, false) } + elsif (parent = from_mod.module_parent) && parent != from_mod && + ! from_mod.module_parents.any? { |p| p.const_defined?(const_name, false) } # If our parents do not have a constant named +const_name+ then we are free # to attempt to load upwards. If they do have such a constant, then this # const_missing must be due to from_mod::const_name, which should not @@ -554,6 +570,7 @@ module ActiveSupport #:nodoc: # as the environment will be in an inconsistent state, e.g. other constants # may have already been unloaded and not accessible. def remove_unloadable_constants! + log("removing unloadable constants") autoloaded_constants.each { |const| remove_constant const } autoloaded_constants.clear Reference.clear! @@ -743,6 +760,10 @@ module ActiveSupport #:nodoc: # The constant is no longer reachable, just skip it. end end + + def log(message) + logger.debug("autoloading: #{message}") if logger && verbose + end end end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 3abd25aa85..725667d139 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -43,7 +43,7 @@ module ActiveSupport deprecation_horizon: deprecation_horizon) }, - silence: ->(message, callstack, deprecation_horizon, gem_name) {}, + silence: ->(message, callstack, deprecation_horizon, gem_name) { }, } # Behavior module allows to determine how to display deprecation messages. diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb index 414f727705..d3233e6111 100644 --- a/activesupport/lib/active_support/duration/iso8601_parser.rb +++ b/activesupport/lib/active_support/duration/iso8601_parser.rb @@ -13,8 +13,8 @@ module ActiveSupport class ParsingError < ::ArgumentError; end PERIOD_OR_COMMA = /\.|,/ - PERIOD = ".".freeze - COMMA = ",".freeze + PERIOD = "." + COMMA = "," SIGN_MARKER = /\A\-|\+|/ DATE_MARKER = /P/ diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb index 84ae29c1ec..1125454919 100644 --- a/activesupport/lib/active_support/duration/iso8601_serializer.rb +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -14,14 +14,14 @@ module ActiveSupport # Builds and returns output string. def serialize parts, sign = normalize - return "PT0S".freeze if parts.empty? + return "PT0S" if parts.empty? - output = "P".dup + output = +"P" output << "#{parts[:years]}Y" if parts.key?(:years) output << "#{parts[:months]}M" if parts.key?(:months) output << "#{parts[:weeks]}W" if parts.key?(:weeks) output << "#{parts[:days]}D" if parts.key?(:days) - time = "".dup + time = +"" time << "#{parts[:hours]}H" if parts.key?(:hours) time << "#{parts[:minutes]}M" if parts.key?(:minutes) if parts.key?(:seconds) diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb index 3c6da10548..cc1d026737 100644 --- a/activesupport/lib/active_support/encrypted_configuration.rb +++ b/activesupport/lib/active_support/encrypted_configuration.rb @@ -39,7 +39,7 @@ module ActiveSupport end def deserialize(config) - config.present? ? YAML.load(config, content_path) : {} + YAML.load(config).presence || {} end end end diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index f48c586cad..ca810db584 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/callbacks" +require "concurrent/hash" module ActiveSupport class ExecutionWrapper diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index e4afc8af93..f1af76019a 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -2,6 +2,7 @@ require "active_support/core_ext/hash/keys" require "active_support/core_ext/hash/reverse_merge" +require "active_support/core_ext/hash/except" module ActiveSupport # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered @@ -279,6 +280,8 @@ module ActiveSupport super(convert_key(key)) end + alias_method :without, :except + def stringify_keys!; self end def deep_stringify_keys!; self end def stringify_keys; dup end @@ -286,6 +289,7 @@ module ActiveSupport undef :symbolize_keys! undef :deep_symbolize_keys! def symbolize_keys; to_hash.symbolize_keys! end + alias_method :to_options, :symbolize_keys def deep_symbolize_keys; to_hash.deep_symbolize_keys! end def to_options!; self end diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 93bde57f6a..584930e413 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -87,9 +87,21 @@ module I18n when Hash, Array Array.wrap(fallbacks) else # TrueClass - [] + [I18n.default_locale] end + if args.empty? || args.first.is_a?(Hash) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using I18n fallbacks with an empty `defaults` sets the defaults to + include the `default_locale`. This behavior will change in Rails 6.1. + If you desire the default locale to be included in the defaults, please + explicitly configure it with `config.i18n.fallbacks.defaults = + [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale, + {...}]` + MSG + args.unshift I18n.default_locale + end + I18n.fallbacks = I18n::Locale::Fallbacks.new(*args) end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 7359de762a..1af9833d46 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -73,7 +73,7 @@ module ActiveSupport string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!("/".freeze, "::".freeze) + string.gsub!("/", "::") string end @@ -90,11 +90,11 @@ module ActiveSupport # camelize(underscore('SSLError')) # => "SslError" def underscore(camel_cased_word) return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word) - word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze) - word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_'.freeze }#{$2.downcase}" } - word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) - word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) - word.tr!("-".freeze, "_".freeze) + word = camel_cased_word.to_s.gsub("::", "/") + word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') + word.tr!("-", "_") word.downcase! word end @@ -130,11 +130,11 @@ module ActiveSupport inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.sub!(/\A_+/, "".freeze) + result.sub!(/\A_+/, "") unless keep_id_suffix - result.sub!(/_id\z/, "".freeze) + result.sub!(/_id\z/, "") end - result.tr!("_".freeze, " ".freeze) + result.tr!("_", " ") result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match.downcase] || match.downcase}" @@ -199,14 +199,14 @@ module ActiveSupport # classify('calculus') # => "Calculus" def classify(table_name) # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, "".freeze))) + camelize(singularize(table_name.to_s.sub(/.*\./, ""))) end # Replaces underscores with dashes in the string. # # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) - underscored_word.tr("_".freeze, "-".freeze) + underscored_word.tr("_", "-") end # Removes the module part from the expression in the string. @@ -269,7 +269,7 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - names = camel_cased_word.split("::".freeze) + names = camel_cased_word.split("::") # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? @@ -364,7 +364,7 @@ module ActiveSupport # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" # const_regexp("::") # => "::" def const_regexp(camel_cased_word) - parts = camel_cased_word.split("::".freeze) + parts = camel_cased_word.split("::") return Regexp.escape(camel_cased_word) if parts.blank? diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 6f2ca4999c..0d2a17970f 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -58,13 +58,13 @@ module ActiveSupport # I18n.locale = :de # transliterate('Jürgen') # # => "Juergen" - def transliterate(string, replacement = "?".freeze) + def transliterate(string, replacement = "?") raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) I18n.transliterate( - ActiveSupport::Multibyte::Unicode.normalize( - ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), - replacement: replacement) + ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc), + replacement: replacement + ) end # Replaces special characters in a string so that it may be used as part of @@ -97,7 +97,7 @@ module ActiveSupport parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator) unless separator.nil? || separator.empty? - if separator == "-".freeze + if separator == "-" re_duplicate_separator = /-{2,}/ re_leading_trailing_separator = /^-|-$/i else @@ -108,7 +108,7 @@ module ActiveSupport # No more than one of the separator in a row. parameterized_string.gsub!(re_duplicate_separator, separator) # Remove leading/trailing separator. - parameterized_string.gsub!(re_leading_trailing_separator, "".freeze) + parameterized_string.gsub!(re_leading_trailing_separator, "") end parameterized_string.downcase! unless preserve_case diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 8152a182b4..b8555c887b 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -6,7 +6,6 @@ require "logger" module ActiveSupport class Logger < ::Logger - include ActiveSupport::LoggerThreadSafeLevel include LoggerSilence # Returns true if the logger destination matches one of the sources @@ -81,20 +80,6 @@ module ActiveSupport def initialize(*args) super @formatter = SimpleFormatter.new - after_initialize if respond_to? :after_initialize - end - - def add(severity, message = nil, progname = nil, &block) - return true if @logdev.nil? || (severity || UNKNOWN) < level - super - end - - Logger::Severity.constants.each do |severity| - class_eval(<<-EOT, __FILE__, __LINE__ + 1) - def #{severity.downcase}? # def debug? - Logger::#{severity} >= level # DEBUG >= level - end # end - EOT end # Simple formatter which only displays the message. diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index 89f32b6782..b2444c1e34 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -2,28 +2,44 @@ require "active_support/concern" require "active_support/core_ext/module/attribute_accessors" -require "concurrent" +require "active_support/logger_thread_safe_level" module LoggerSilence extend ActiveSupport::Concern included do - cattr_accessor :silencer, default: true + ActiveSupport::Deprecation.warn( + "Including LoggerSilence is deprecated and will be removed in Rails 6.1. " \ + "Please use `ActiveSupport::LoggerSilence` instead" + ) + + include ActiveSupport::LoggerSilence end +end + +module ActiveSupport + module LoggerSilence + extend ActiveSupport::Concern + + included do + cattr_accessor :silencer, default: true + include ActiveSupport::LoggerThreadSafeLevel + end - # Silences the logger for the duration of the block. - def silence(temporary_level = Logger::ERROR) - if silencer - begin - old_local_level = local_level - self.local_level = temporary_level + # Silences the logger for the duration of the block. + def silence(temporary_level = Logger::ERROR) + if silencer + begin + old_local_level = local_level + self.local_level = temporary_level + yield self + ensure + self.local_level = old_local_level + end + else yield self - ensure - self.local_level = old_local_level end - else - yield self end end end diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb index ba32813d3d..f16c90cfc6 100644 --- a/activesupport/lib/active_support/logger_thread_safe_level.rb +++ b/activesupport/lib/active_support/logger_thread_safe_level.rb @@ -1,13 +1,30 @@ # frozen_string_literal: true require "active_support/concern" +require "active_support/core_ext/module/attribute_accessors" +require "concurrent" module ActiveSupport module LoggerThreadSafeLevel # :nodoc: extend ActiveSupport::Concern + included do + cattr_accessor :local_levels, default: Concurrent::Map.new(initial_capacity: 2), instance_accessor: false + end + + Logger::Severity.constants.each do |severity| + class_eval(<<-EOT, __FILE__, __LINE__ + 1) + def #{severity.downcase}? # def debug? + Logger::#{severity} >= level # DEBUG >= level + end # end + EOT + end + def after_initialize - @local_levels = Concurrent::Map.new(initial_capacity: 2) + ActiveSupport::Deprecation.warn( + "Logger don't need to call #after_initialize directly anymore. It will be deprecated without replacement in " \ + "Rails 6.1." + ) end def local_log_id @@ -15,19 +32,24 @@ module ActiveSupport end def local_level - @local_levels[local_log_id] + self.class.local_levels[local_log_id] end def local_level=(level) if level - @local_levels[local_log_id] = level + self.class.local_levels[local_log_id] = level else - @local_levels.delete(local_log_id) + self.class.local_levels.delete(local_log_id) end end def level local_level || super end + + def add(severity, message = nil, progname = nil, &block) # :nodoc: + return true if @logdev.nil? || (severity || UNKNOWN) < level + super + end end end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 404404cad1..6f7302e732 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -182,7 +182,7 @@ module ActiveSupport def _decrypt(encrypted_message, purpose) cipher = new_cipher - encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) } + encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) } # Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 83c39c0a86..64c557bec6 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -122,7 +122,7 @@ module ActiveSupport def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? - data, digest = signed_message.split("--".freeze) + data, digest = signed_message.split("--") data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end @@ -150,7 +150,7 @@ module ActiveSupport def verified(signed_message, purpose: nil, **) if valid_message?(signed_message) begin - data = signed_message.split("--".freeze)[0] + data = signed_message.split("--")[0] message = Messages::Metadata.verify(decode(data), purpose) @serializer.load(message) if message rescue ArgumentError => argument_error diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 499a206f49..a1e23aeaca 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -17,7 +17,7 @@ module ActiveSupport #:nodoc: # through the +mb_chars+ method. Methods which would normally return a # String object now return a Chars object so methods can be chained. # - # 'The Perfect String '.mb_chars.downcase.strip.normalize + # 'The Perfect String '.mb_chars.downcase.strip # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string"> # # Chars objects are perfectly interchangeable with String objects as long as @@ -76,6 +76,11 @@ module ActiveSupport #:nodoc: # Returns +true+ when the proxy class can handle the string. Returns # +false+ otherwise. def self.consumes?(string) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be + removed from Rails 6.1. Use string.is_utf8? instead. + MSG + string.encoding == Encoding::UTF_8 end @@ -108,7 +113,7 @@ module ActiveSupport #:nodoc: # # 'Café'.mb_chars.reverse.to_s # => 'éfaC' def reverse - chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack("U*")) + chars(@wrapped_string.scan(/\X/).reverse.join) end # Limits the byte size of the string to a number of bytes without breaking @@ -117,35 +122,7 @@ module ActiveSupport #:nodoc: # # 'こんにちは'.mb_chars.limit(7).to_s # => "こん" def limit(limit) - slice(0...translate_offset(limit)) - end - - # Converts characters in the string to uppercase. - # - # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?" - def upcase - chars Unicode.upcase(@wrapped_string) - end - - # Converts characters in the string to lowercase. - # - # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum" - def downcase - chars Unicode.downcase(@wrapped_string) - end - - # Converts characters in the string to the opposite case. - # - # 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN" - def swapcase - chars Unicode.swapcase(@wrapped_string) - end - - # Converts the first character to uppercase and the remainder to lowercase. - # - # 'über'.mb_chars.capitalize.to_s # => "Über" - def capitalize - (slice(0) || chars("")).upcase + (slice(1..-1) || chars("")).downcase + truncate_bytes(limit, omission: nil) end # Capitalizes the first letter of every word, when possible. @@ -153,7 +130,7 @@ module ActiveSupport #:nodoc: # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró" # "日本語".mb_chars.titleize.to_s # => "日本語" def titleize - chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) }) + chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase }) end alias_method :titlecase, :titleize @@ -165,7 +142,24 @@ module ActiveSupport #:nodoc: # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is # ActiveSupport::Multibyte::Unicode.default_normalization_form def normalize(form = nil) - chars(Unicode.normalize(@wrapped_string, form)) + form ||= Unicode.default_normalization_form + + # See https://www.unicode.org/reports/tr15, Table 1 + if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars#normalize is deprecated and will be + removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead. + MSG + + send(:unicode_normalize, alias_form) + else + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars#normalize is deprecated and will be + removed from Rails 6.1. Use #unicode_normalize instead. + MSG + + raise ArgumentError, "#{form} is not a valid normalization variant", caller + end end # Performs canonical decomposition on all the characters. @@ -189,7 +183,7 @@ module ActiveSupport #:nodoc: # 'क्षि'.mb_chars.length # => 4 # 'क्षि'.mb_chars.grapheme_length # => 3 def grapheme_length - Unicode.unpack_graphemes(@wrapped_string).length + @wrapped_string.scan(/\X/).length end # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent @@ -205,7 +199,7 @@ module ActiveSupport #:nodoc: to_s.as_json(options) end - %w(capitalize downcase reverse tidy_bytes upcase).each do |method| + %w(reverse tidy_bytes).each do |method| define_method("#{method}!") do |*args| @wrapped_string = send(method, *args).to_s self @@ -214,18 +208,6 @@ module ActiveSupport #:nodoc: private - def translate_offset(byte_offset) - return nil if byte_offset.nil? - return 0 if @wrapped_string == "" - - begin - @wrapped_string.byteslice(0...byte_offset).unpack("U*").length - rescue ArgumentError - byte_offset -= 1 - retry - end - end - def chars(string) self.class.new(string) end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 4f0e1165ef..ce8ecece69 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -6,10 +6,17 @@ module ActiveSupport extend self # A list of all available normalization forms. - # See http://www.unicode.org/reports/tr15/tr15-29.html for more + # See https://www.unicode.org/reports/tr15/tr15-29.html for more # information about normalization. NORMALIZATION_FORMS = [:c, :kc, :d, :kd] + NORMALIZATION_FORM_ALIASES = { # :nodoc: + c: :nfc, + d: :nfd, + kc: :nfkc, + kd: :nfkd + } + # The Unicode version that is supported by the implementation UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"] @@ -27,6 +34,11 @@ module ActiveSupport # Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]] # Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]] def unpack_graphemes(string) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#unpack_graphemes is deprecated and will be + removed from Rails 6.1. Use string.scan(/\X/).map(&:codepoints) instead. + MSG + string.scan(/\X/).map(&:codepoints) end @@ -34,6 +46,11 @@ module ActiveSupport # # Unicode.pack_graphemes(Unicode.unpack_graphemes('क्षि')) # => 'क्षि' def pack_graphemes(unpacked) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#pack_graphemes is deprecated and will be + removed from Rails 6.1. Use array.flatten.pack("U*") instead. + MSG + unpacked.flatten.pack("U*") end @@ -100,31 +117,34 @@ module ActiveSupport # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. def normalize(string, form = nil) form ||= @default_normalization_form - # See http://www.unicode.org/reports/tr15, Table 1 - case form - when :d - string.unicode_normalize(:nfd) - when :c - string.unicode_normalize(:nfc) - when :kd - string.unicode_normalize(:nfkd) - when :kc - string.unicode_normalize(:nfkc) + + # See https://www.unicode.org/reports/tr15, Table 1 + if alias_form = NORMALIZATION_FORM_ALIASES[form] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be + removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead. + MSG + + string.unicode_normalize(alias_form) else + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be + removed from Rails 6.1. Use String#unicode_normalize instead. + MSG + raise ArgumentError, "#{form} is not a valid normalization variant", caller end end - def downcase(string) - string.downcase - end + %w(downcase upcase swapcase).each do |method| + define_method(method) do |string| + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode##{method} is deprecated and + will be removed from Rails 6.1. Use String methods directly. + MSG - def upcase(string) - string.upcase - end - - def swapcase(string) - string.swapcase + string.send(method) + end end private diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 6207de8094..01cc363e2b 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -34,7 +34,7 @@ module ActiveSupport # name # => String, name of the event (such as 'render' from above) # start # => Time, when the instrumented block started execution # finish # => Time, when the instrumented block ended execution - # id # => String, unique ID for this notification + # id # => String, unique ID for the instrumenter that fired the event # payload # => Hash, the payload # end # @@ -59,7 +59,7 @@ module ActiveSupport # event.payload # => { extra: :information } # # The block in the <tt>subscribe</tt> call gets the name of the event, start - # timestamp, end timestamp, a string with a unique identifier for that event + # timestamp, end timestamp, a string with a unique identifier for that event's instrumenter # (something like "535801666f04d0298cd6"), and a hash with the payload, in # that order. # @@ -171,6 +171,24 @@ module ActiveSupport end end + # Subscribe to a given event name with the passed +block+. + # + # You can subscribe to events by passing a String to match exact event + # names, or by passing a Regexp to match all events that match a pattern. + # + # ActiveSupport::Notifications.subscribe(/render/) do |*args| + # ... + # end + # + # The +block+ will receive five parameters with information about the event: + # + # ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload| + # name # => String, name of the event (such as 'render' from above) + # start # => Time, when the instrumented block started execution + # finish # => Time, when the instrumented block ended execution + # id # => String, unique ID for the instrumenter that fired the event + # payload # => Hash, the payload + # end def subscribe(*args, &block) notifier.subscribe(*args, &block) end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 8fd6e932f1..d19a2f64d4 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/dependencies/autoload" + module ActiveSupport module NumberHelper extend ActiveSupport::Autoload @@ -85,6 +87,9 @@ module ActiveSupport # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). # # ==== Examples # @@ -100,6 +105,8 @@ module ActiveSupport # # => "£1234567890,50" # number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '', format: '%n %u') # # => "1234567890,50 £" + # number_to_currency(1234567890.50, strip_insignificant_zeros: true) + # # => "$1,234,567,890.5" def number_to_currency(number, options = {}) NumberToCurrencyConverter.convert(number, options) end diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index a25e22cbd3..0e8ae82dd5 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToCurrencyConverter < NumberConverter # :nodoc: @@ -15,7 +17,7 @@ module ActiveSupport end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, options[:unit]) + format.gsub("%n", rounded_number).gsub("%u", options[:unit]) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb index d5b5706705..467a580a2e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToDelimitedConverter < NumberConverter #:nodoc: @@ -14,7 +16,7 @@ module ActiveSupport private def parts - left, right = number.to_s.split(".".freeze) + left, right = number.to_s.split(".") left.gsub!(delimiter_pattern) do |digit_to_delimit| "#{digit_to_delimit}#{options[:delimiter]}" end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 03eb6671ec..494408fc01 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToHumanConverter < NumberConverter # :nodoc: @@ -25,7 +27,7 @@ module ActiveSupport rounded_number = NumberToRoundedConverter.convert(number, options) unit = determine_unit(units, exponent) - format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip + format.gsub("%n", rounded_number).gsub("%u", unit).strip end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 842f2fc8df..91262fa656 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToHumanSizeConverter < NumberConverter #:nodoc: @@ -22,7 +24,7 @@ module ActiveSupport human_size = number / (base**exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end - conversion_format.gsub("%n".freeze, number_to_format).gsub("%u".freeze, unit) + conversion_format.gsub("%n", number_to_format).gsub("%u", unit) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 4dcdad2e2c..0c2e190f8a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToPercentageConverter < NumberConverter # :nodoc: @@ -7,7 +9,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub("%n".freeze, rounded_number) + options[:format].gsub("%n", rounded_number) end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index 96410f4995..d5e72981b4 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToPhoneConverter < NumberConverter #:nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index eb528a0583..6ceb9a572e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToRoundedConverter < NumberConverter # :nodoc: @@ -20,9 +22,9 @@ module ActiveSupport formatted_string = if BigDecimal === rounded_number && rounded_number.finite? s = rounded_number.to_s("F") - s << "0".freeze * precision - a, b = s.split(".".freeze, 2) - a << ".".freeze + s << "0" * precision + a, b = s.split(".", 2) + a << "." a << b[0, precision] else "%00.#{precision}f" % rounded_number diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb new file mode 100644 index 0000000000..1389d82523 --- /dev/null +++ b/activesupport/lib/active_support/parameter_filter.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/duplicable" +require "active_support/core_ext/array/extract" + +module ActiveSupport + # +ParameterFilter+ allows you to specify keys for sensitive data from + # hash-like object and replace corresponding value. Filtering only certain + # sub-keys from a hash is possible by using the dot notation: + # 'credit_card.number'. If a proc is given, each key and value of a hash and + # all sub-hashes are passed to it, where the value or the key can be replaced + # using String#replace or similar methods. + # + # ActiveSupport::ParameterFilter.new([:password]) + # => replaces the value to all keys matching /password/i with "[FILTERED]" + # + # ActiveSupport::ParameterFilter.new([:foo, "bar"]) + # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + # + # ActiveSupport::ParameterFilter.new(["credit_card.code"]) + # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not + # change { file: { code: "xxxx"} } + # + # ActiveSupport::ParameterFilter.new([-> (k, v) do + # v.reverse! if k =~ /secret/i + # end]) + # => reverses the value to all keys matching /secret/i + class ParameterFilter + FILTERED = "[FILTERED]" # :nodoc: + + # Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+. + # Other types of filters are treated as +String+ using +to_s+. + # For +Proc+ filters, key, value, and optional original hash is passed to block arguments. + # + # ==== Options + # + # * <tt>:mask</tt> - A replaced object when filtered. Defaults to +"[FILTERED]"+ + def initialize(filters = [], mask: FILTERED) + @filters = filters + @mask = mask + end + + # Mask value of +params+ if key matches one of filters. + def filter(params) + compiled_filter.call(params) + end + + # Returns filtered value for given key. For +Proc+ filters, third block argument is not populated. + def filter_param(key, value) + @filters.empty? ? value : compiled_filter.value_for_key(key, value) + end + + private + + def compiled_filter + @compiled_filter ||= CompiledFilter.compile(@filters, mask: @mask) + end + + class CompiledFilter # :nodoc: + def self.compile(filters, mask:) + return lambda { |params| params.dup } if filters.empty? + + strings, regexps, blocks = [], [], [] + + filters.each do |item| + case item + when Proc + blocks << item + when Regexp + regexps << item + else + strings << Regexp.escape(item.to_s) + end + end + + deep_regexps = regexps.extract! { |r| r.to_s.include?("\\.") } + deep_strings = strings.extract! { |s| s.include?("\\.") } + + regexps << Regexp.new(strings.join("|"), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join("|"), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks, mask: mask + end + + attr_reader :regexps, :deep_regexps, :blocks + + def initialize(regexps, deep_regexps, blocks, mask:) + @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil + @blocks = blocks + @mask = mask + end + + def call(params, parents = [], original_params = params) + filtered_params = params.class.new + + params.each do |key, value| + filtered_params[key] = value_for_key(key, value, parents, original_params) + end + + filtered_params + end + + def value_for_key(key, value, parents = [], original_params = nil) + parents.push(key) if deep_regexps + if regexps.any? { |r| r.match?(key) } + value = @mask + elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| r.match?(joined) } + value = @mask + elsif value.is_a?(Hash) + value = call(value, parents, original_params) + elsif value.is_a?(Array) + value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v } + elsif blocks.any? + key = key.dup if key.duplicable? + value = value.dup if value.duplicable? + blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) } + end + parents.pop if deep_regexps + value + end + end + end +end diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index b26d9c3665..fea18e9712 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/execution_wrapper" +require "active_support/executor" module ActiveSupport #-- diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 5a4c3d74af..f3e902f9dd 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -79,12 +79,12 @@ module ActiveSupport end def start(name, id, payload) - e = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload) - e.start! + event = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload) + event.start! parent = event_stack.last - parent << e if parent + parent << event if parent - event_stack.push e + event_stack.push event end def finish(name, id, payload) @@ -92,7 +92,7 @@ module ActiveSupport event.finish! event.payload.merge!(payload) - method = name.split(".".freeze).first + method = name.split(".").first send(method, event) end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index b069ac94d4..d8a86d997e 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -46,7 +46,7 @@ module ActiveSupport def current_tags # We use our object ID here to avoid conflicting with other instances - thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze + thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}" Thread.current[thread_key] ||= [] end @@ -61,8 +61,15 @@ module ActiveSupport end def self.new(logger) - # Ensure we set a default formatter so we aren't extending nil! - logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new + logger = logger.dup + + if logger.formatter + logger.formatter = logger.formatter.dup + else + # Ensure we set a default formatter so we aren't extending nil! + logger.formatter = ActiveSupport::Logger::SimpleFormatter.new + end + logger.formatter.extend Formatter logger.extend(self) end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index f17743b6db..ef12c6b9b0 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -65,8 +65,8 @@ module ActiveSupport # # parallelize(workers: 2, with: :threads) # - # The threaded parallelization uses Minitest's parallel executor directly. - # The processes parallelization uses a Ruby Drb server. + # The threaded parallelization uses minitest's parallel executor directly. + # The processes parallelization uses a Ruby DRb server. def parallelize(workers: 2, with: :processes) workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"] diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb index ad923d1aab..4eb7a88576 100644 --- a/activesupport/lib/active_support/testing/file_fixtures.rb +++ b/activesupport/lib/active_support/testing/file_fixtures.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/concern" + module ActiveSupport module Testing # Adds simple access to sample files called file fixtures. diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb index c6358002ea..eba41aa907 100644 --- a/activesupport/lib/active_support/testing/method_call_assertions.rb +++ b/activesupport/lib/active_support/testing/method_call_assertions.rb @@ -17,7 +17,7 @@ module ActiveSupport assert_equal times, times_called, error end - def assert_called_with(object, method_name, args = [], returns: nil) + def assert_called_with(object, method_name, args, returns: nil) mock = Minitest::Mock.new if args.all? { |arg| arg.is_a?(Array) } @@ -35,6 +35,35 @@ module ActiveSupport assert_called(object, method_name, message, times: 0, &block) end + # TODO: No need to resort to #send once support for Ruby 2.4 is + # dropped. + def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil) + times_called = 0 + klass.send(:define_method, "stubbed_#{method_name}") do |*| + times_called += 1 + + returns + end + + klass.send(:alias_method, "original_#{method_name}", method_name) + klass.send(:alias_method, method_name, "stubbed_#{method_name}") + + yield + + error = "Expected #{method_name} to be called #{times} times, but was called #{times_called} times" + error = "#{message}.\n#{error}" if message + + assert_equal times, times_called, error + ensure + klass.send(:alias_method, method_name, "original_#{method_name}") + klass.send(:undef_method, "original_#{method_name}") + klass.send(:undef_method, "stubbed_#{method_name}") + end + + def assert_not_called_on_instance_of(klass, method_name, message = nil, &block) + assert_called_on_instance_of(klass, method_name, message, times: 0, &block) + end + def stub_any_instance(klass, instance: klass.new) klass.stub(:new, instance) { yield instance } end diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb index 1caac1feb3..9c8dffa9d8 100644 --- a/activesupport/lib/active_support/testing/parallelization.rb +++ b/activesupport/lib/active_support/testing/parallelization.rb @@ -2,6 +2,7 @@ require "drb" require "drb/unix" +require "active_support/core_ext/module/attribute_accessors" module ActiveSupport module Testing @@ -65,22 +66,31 @@ module ActiveSupport def start @pool = @queue_size.times.map do |worker| fork do - DRb.stop_service - - after_fork(worker) - - queue = DRbObject.new_with_uri(@url) - - while job = queue.pop - klass = job[0] - method = job[1] - reporter = job[2] - result = Minitest.run_one_method(klass, method) - - queue.record(reporter, result) + begin + DRb.stop_service + + after_fork(worker) + + queue = DRbObject.new_with_uri(@url) + + while job = queue.pop + klass = job[0] + method = job[1] + reporter = job[2] + result = Minitest.run_one_method(klass, method) + + begin + queue.record(reporter, result) + rescue DRb::DRbConnError + result.failures.each do |failure| + failure.exception = DRb::DRbRemoteError.new(failure.exception) + end + queue.record(reporter, result) + end + end + ensure + run_cleanup(worker) end - - run_cleanup(worker) end end end diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 801ea2909b..f160e66971 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -158,7 +158,7 @@ module ActiveSupport end # Returns the current time back to its original state, by removing the stubs added by - # +travel+ and +travel_to+. + # +travel+, +travel_to+, and +freeze_time+. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) @@ -168,6 +168,7 @@ module ActiveSupport def travel_back simple_stubs.unstub_all! end + alias_method :unfreeze_time, :travel_back # Calls +travel_to+ with +Time.now+. # diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 7e71318404..3be5f6f7b5 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -43,8 +43,8 @@ module ActiveSupport "Time" end - PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze } - PRECISIONS[0] = "%FT%T".freeze + PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N" } + PRECISIONS[0] = "%FT%T" include Comparable, DateAndTime::Compatibility attr_reader :time_zone @@ -147,7 +147,7 @@ module ActiveSupport # # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) - "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}" + "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z')}" end alias_method :iso8601, :xmlschema alias_method :rfc3339, :xmlschema @@ -286,8 +286,10 @@ module ActiveSupport alias_method :since, :+ alias_method :in, :+ - # Returns a new TimeWithZone object that represents the difference between - # the current object's time and the +other+ time. + # Subtracts an interval of time and returns a new TimeWithZone object unless + # the other value `acts_like?` time. Then it will return a Float of the difference + # between the two times that represents the difference between the current + # object's time and the +other+ time. # # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00 @@ -302,6 +304,12 @@ module ActiveSupport # # now - 24.hours # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 # now - 1.day # => Sun, 02 Nov 2014 00:26:28 EDT -04:00 + # + # If both the TimeWithZone object and the other value act like Time, a Float + # will be returned. + # + # Time.zone.now - 1.day.ago # => 86399.999967 + # def -(other) if other.acts_like?(:time) to_time - other.to_time diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index fd07a3a6a2..d9e033e23b 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -182,8 +182,9 @@ module ActiveSupport "Samoa" => "Pacific/Apia" } - UTC_OFFSET_WITH_COLON = "%s%02d:%02d" - UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") + UTC_OFFSET_WITH_COLON = "%s%02d:%02d" # :nodoc: + UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") # :nodoc: + private_constant :UTC_OFFSET_WITH_COLON, :UTC_OFFSET_WITHOUT_COLON @lazy_zones_map = Concurrent::Map.new @country_zones = Concurrent::Map.new diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 7f94a64016..32fe6ade28 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -18,7 +18,7 @@ module ActiveSupport module XmlMini_JDOM #:nodoc: extend self - CONTENT_KEY = "__content__".freeze + CONTENT_KEY = "__content__" NODE_TYPE_NAMES = %w{ATTRIBUTE_NODE CDATA_SECTION_NODE COMMENT_NODE DOCUMENT_FRAGMENT_NODE DOCUMENT_NODE DOCUMENT_TYPE_NODE ELEMENT_NODE ENTITY_NODE ENTITY_REFERENCE_NODE NOTATION_NODE @@ -169,7 +169,7 @@ module ActiveSupport # element:: # XML element to be checked. def empty_content?(element) - text = "".dup + text = +"" child_nodes = element.child_nodes (0...child_nodes.length).each do |i| item = child_nodes.item(i) diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 0b000fea60..c2e999ef6c 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -34,7 +34,7 @@ module LibXML #:nodoc: end module Node #:nodoc: - CONTENT_ROOT = "__content__".freeze + CONTENT_ROOT = "__content__" # Convert XML document to hash. # @@ -55,7 +55,7 @@ module LibXML #:nodoc: if c.element? c.to_hash(node_hash) elsif c.text? || c.cdata? - node_hash[CONTENT_ROOT] ||= "".dup + node_hash[CONTENT_ROOT] ||= +"" node_hash[CONTENT_ROOT] << c.content end end diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index dcf16e6084..ac8acdfc3c 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -13,8 +13,8 @@ module ActiveSupport class HashBuilder include LibXML::XML::SaxParser::Callbacks - CONTENT_KEY = "__content__".freeze - HASH_SIZE_KEY = "__hash_size__".freeze + CONTENT_KEY = "__content__" + HASH_SIZE_KEY = "__hash_size__" attr_reader :hash @@ -23,7 +23,7 @@ module ActiveSupport end def on_start_document - @hash = { CONTENT_KEY => "".dup } + @hash = { CONTENT_KEY => +"" } @hash_stack = [@hash] end @@ -33,7 +33,7 @@ module ActiveSupport end def on_start_element(name, attrs = {}) - new_hash = { CONTENT_KEY => "".dup }.merge!(attrs) + new_hash = { CONTENT_KEY => +"" }.merge!(attrs) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index 5ee6fc8159..f76513f48b 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -38,7 +38,7 @@ module ActiveSupport end module Node #:nodoc: - CONTENT_ROOT = "__content__".freeze + CONTENT_ROOT = "__content__" # Convert XML document to hash. # @@ -59,7 +59,7 @@ module ActiveSupport if c.element? c.to_hash(node_hash) elsif c.text? || c.cdata? - node_hash[CONTENT_ROOT] ||= "".dup + node_hash[CONTENT_ROOT] ||= +"" node_hash[CONTENT_ROOT] << c.content end end diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index b01ed00a14..55cd72e093 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -16,8 +16,8 @@ module ActiveSupport # Class that will build the hash while the XML document # is being parsed using SAX events. class HashBuilder < Nokogiri::XML::SAX::Document - CONTENT_KEY = "__content__".freeze - HASH_SIZE_KEY = "__hash_size__".freeze + CONTENT_KEY = "__content__" + HASH_SIZE_KEY = "__hash_size__" attr_reader :hash @@ -39,7 +39,7 @@ module ActiveSupport end def start_element(name, attrs = []) - new_hash = { CONTENT_KEY => "".dup }.merge!(Hash[attrs]) + new_hash = { CONTENT_KEY => +"" }.merge!(Hash[attrs]) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 32458d5b0d..8d6e3af066 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -8,7 +8,7 @@ module ActiveSupport module XmlMini_REXML #:nodoc: extend self - CONTENT_KEY = "__content__".freeze + CONTENT_KEY = "__content__" # Parse an XML Document string or IO into a simple hash. # @@ -76,7 +76,7 @@ module ActiveSupport hash else # must use value to prevent double-escaping - texts = "".dup + texts = +"" element.texts.each { |t| texts << t.value } merge!(hash, CONTENT_KEY, texts) end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index f214898145..168d3655d3 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -29,17 +29,18 @@ I18n.enforce_available_locales = false class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end - - def frozen_error_class - Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end + + def frozen_error_class + Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError + end end diff --git a/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb b/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb new file mode 100644 index 0000000000..f9d6e675d7 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "dependencies/module_folder/lib_class" + +module ModuleFolder + class NestedWithRequire + end +end diff --git a/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb b/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb new file mode 100644 index 0000000000..e8fb321077 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class NestedWithRequireParent + ModuleFolder::NestedWithRequire +end diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb index cb7a69cccf..59a71d99be 100644 --- a/activesupport/test/benchmarkable_test.rb +++ b/activesupport/test/benchmarkable_test.rb @@ -59,13 +59,13 @@ class BenchmarkableTest < ActiveSupport::TestCase def test_within_level logger.level = ActiveSupport::Logger::DEBUG - benchmark("included_debug_run", level: :debug) {} + benchmark("included_debug_run", level: :debug) { } assert_last_logged "included_debug_run" end def test_outside_level logger.level = ActiveSupport::Logger::ERROR - benchmark("skipped_debug_run", level: :debug) {} + benchmark("skipped_debug_run", level: :debug) { } assert_no_match(/skipped_debug_run/, buffer.last) ensure logger.level = ActiveSupport::Logger::DEBUG diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index 181113e70a..7dfa8a62bd 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -114,7 +114,17 @@ module ActiveSupport assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds end + test "Including top constant LoggerSilence is deprecated" do + assert_deprecated("Please use `ActiveSupport::LoggerSilence`") do + Class.new(CustomLogger) do + include ::LoggerSilence + end + end + end + class CustomLogger + include ActiveSupport::LoggerSilence + attr_reader :adds, :closed, :chevrons attr_accessor :level, :progname, :formatter, :local_level @@ -166,7 +176,6 @@ module ActiveSupport end class FakeLogger < CustomLogger - include LoggerSilence end end end diff --git a/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb index 4e8ff60eb3..a4abdd37b9 100644 --- a/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb @@ -2,7 +2,7 @@ module CacheInstrumentationBehavior def test_fetch_multi_uses_write_multi_entries_store_provider_interface - assert_called_with(@cache, :write_multi_entries) do + assert_called(@cache, :write_multi_entries) do @cache.fetch_multi "a", "b", "c" do |key| key * 2 end diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index ae272e87d7..9f54b1e7de 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -148,7 +148,7 @@ module CacheStoreBehavior end end - # Use strings that are guarenteed to compress well, so we can easily tell if + # Use strings that are guaranteed to compress well, so we can easily tell if # the compression kicked in or not. SMALL_STRING = "0" * 100 LARGE_STRING = "0" * 2.kilobytes @@ -290,6 +290,55 @@ module CacheStoreBehavior assert_equal "bar", @cache.read("fu/foo") end + InstanceTest = Struct.new(:name, :id) do + def cache_key + "#{name}/#{id}" + end + + def to_param + "hello" + end + end + + def test_array_with_single_instance_as_cache_key_uses_cache_key_method + test_instance_one = InstanceTest.new("test", 1) + test_instance_two = InstanceTest.new("test", 2) + + @cache.write([test_instance_one], "one") + @cache.write([test_instance_two], "two") + + assert_equal "one", @cache.read([test_instance_one]) + assert_equal "two", @cache.read([test_instance_two]) + end + + def test_array_with_multiple_instances_as_cache_key_uses_cache_key_method + test_instance_one = InstanceTest.new("test", 1) + test_instance_two = InstanceTest.new("test", 2) + test_instance_three = InstanceTest.new("test", 3) + + @cache.write([test_instance_one, test_instance_three], "one") + @cache.write([test_instance_two, test_instance_three], "two") + + assert_equal "one", @cache.read([test_instance_one, test_instance_three]) + assert_equal "two", @cache.read([test_instance_two, test_instance_three]) + end + + def test_format_of_expanded_key_for_single_instance + test_instance_one = InstanceTest.new("test", 1) + + expanded_key = @cache.send(:expanded_key, test_instance_one) + + assert_equal expanded_key, test_instance_one.cache_key + end + + def test_format_of_expanded_key_for_single_instance_in_array + test_instance_one = InstanceTest.new("test", 1) + + expanded_key = @cache.send(:expanded_key, [test_instance_one]) + + assert_equal expanded_key, test_instance_one.cache_key + end + def test_hash_as_cache_key @cache.write({ foo: 1, fu: 2 }, "bar") assert_equal "bar", @cache.read("foo=1/fu=2") @@ -319,7 +368,7 @@ module CacheStoreBehavior end def test_original_store_objects_should_not_be_immutable - bar = "bar".dup + bar = +"bar" @cache.write("foo", bar) assert_nothing_raised { bar.gsub!(/.*/, "baz") } end @@ -424,7 +473,7 @@ module CacheStoreBehavior @events << ActiveSupport::Notifications::Event.new(*args) end assert @cache.write(key, "1", raw: true) - assert @cache.fetch(key) {} + assert @cache.fetch(key) { } assert_equal 1, @events.length assert_equal "cache_read.active_support", @events[0].name assert_equal :fetch, @events[0].payload[:super_operation] @@ -438,7 +487,7 @@ module CacheStoreBehavior ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end - assert_not @cache.fetch("bad_key") {} + assert_not @cache.fetch("bad_key") { } assert_equal 3, @events.length assert_equal "cache_read.active_support", @events[0].name assert_equal "cache_generate.active_support", @events[1].name diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb index da16142496..842400f4a3 100644 --- a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb +++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb @@ -6,7 +6,7 @@ module EncodedKeyCacheBehavior Encoding.list.each do |encoding| define_method "test_#{encoding.name.underscore}_encoded_values" do - key = "foo".dup.force_encoding(encoding) + key = (+"foo").force_encoding(encoding) assert @cache.write(key, "1", raw: true) assert_equal "1", @cache.read(key) assert_equal "1", @cache.fetch(key) @@ -18,7 +18,7 @@ module EncodedKeyCacheBehavior end def test_common_utf8_values - key = "\xC3\xBCmlaut".dup.force_encoding(Encoding::UTF_8) + key = (+"\xC3\xBCmlaut").force_encoding(Encoding::UTF_8) assert @cache.write(key, "1", raw: true) assert_equal "1", @cache.read(key) assert_equal "1", @cache.fetch(key) @@ -29,7 +29,7 @@ module EncodedKeyCacheBehavior end def test_retains_encoding - key = "\xC3\xBCmlaut".dup.force_encoding(Encoding::UTF_8) + key = (+"\xC3\xBCmlaut").force_encoding(Encoding::UTF_8) assert @cache.write(key, "1", raw: true) assert_equal Encoding::UTF_8, key.encoding end diff --git a/activesupport/test/cache/cache_key_test.rb b/activesupport/test/cache/cache_key_test.rb index 84e656f504..c2240d03c2 100644 --- a/activesupport/test/cache/cache_key_test.rb +++ b/activesupport/test/cache/cache_key_test.rb @@ -47,7 +47,7 @@ class CacheKeyTest < ActiveSupport::TestCase end def test_expand_cache_key_respond_to_cache_key - key = "foo".dup + key = +"foo" def key.cache_key :foo_key end @@ -55,7 +55,7 @@ class CacheKeyTest < ActiveSupport::TestCase end def test_expand_cache_key_array_with_something_that_responds_to_cache_key - key = "foo".dup + key = +"foo" def key.cache_key :foo_key end diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb index e59fae0b4c..e46fa59784 100644 --- a/activesupport/test/cache/local_cache_middleware_test.rb +++ b/activesupport/test/cache/local_cache_middleware_test.rb @@ -17,7 +17,7 @@ module ActiveSupport }) _, _, body = middleware.call({}) assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.each {} + body.each { } assert LocalCacheRegistry.cache_for(key), "should still have a cache" body.close assert_nil LocalCacheRegistry.cache_for(key) diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 5c9a3b29e7..466b364e9d 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -953,7 +953,7 @@ module CallbacksTest def test_proc_arity_2 assert_raises(ArgumentError) do - klass = build_class(->(x, y) {}) + klass = build_class(->(x, y) { }) klass.new.run end end @@ -1032,7 +1032,7 @@ module CallbacksTest def test_proc_arity2 assert_raises(ArgumentError) do - object = build_class(->(a, b) {}).new + object = build_class(->(a, b) { }).new object.run end end diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb index 1b44c7c9bf..a0a7056952 100644 --- a/activesupport/test/clean_backtrace_test.rb +++ b/activesupport/test/clean_backtrace_test.rb @@ -74,3 +74,43 @@ class BacktraceCleanerFilterAndSilencerTest < ActiveSupport::TestCase assert_equal [ "/class.rb" ], @bc.clean([ "/mongrel/class.rb" ]) end end + +class BacktraceCleanerDefaultFilterAndSilencerTest < ActiveSupport::TestCase + def setup + @bc = ActiveSupport::BacktraceCleaner.new + end + + test "should format installed gems correctly" do + backtrace = [ "#{Gem.default_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] + end + + test "should format installed gems not in Gem.default_dir correctly" do + target_dir = Gem.path.detect { |p| p != Gem.default_dir } + # skip this test if default_dir is the only directory on Gem.path + if target_dir + backtrace = [ "#{target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] + end + end + + test "should format gems installed by bundler" do + backtrace = [ "#{Gem.default_dir}/bundler/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] + end + + test "should silence gems from the backtrace" do + backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace) + assert_empty result + end + + test "should silence stdlib" do + backtrace = ["#{RbConfig::CONFIG["rubylibdir"]}/lib/foo.rb"] + result = @bc.clean(backtrace) + assert_empty result + end +end diff --git a/activesupport/test/core_ext/array/extract_test.rb b/activesupport/test/core_ext/array/extract_test.rb new file mode 100644 index 0000000000..f26e055033 --- /dev/null +++ b/activesupport/test/core_ext/array/extract_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/array" + +class ExtractTest < ActiveSupport::TestCase + def test_extract + numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + array_id = numbers.object_id + + odd_numbers = numbers.extract!(&:odd?) + + assert_equal [1, 3, 5, 7, 9], odd_numbers + assert_equal [0, 2, 4, 6, 8], numbers + assert_equal array_id, numbers.object_id + end + + def test_extract_without_block + numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + array_id = numbers.object_id + + extract_enumerator = numbers.extract! + + assert_instance_of Enumerator, extract_enumerator + assert_equal numbers.size, extract_enumerator.size + + odd_numbers = extract_enumerator.each(&:odd?) + + assert_equal [1, 3, 5, 7, 9], odd_numbers + assert_equal [0, 2, 4, 6, 8], numbers + assert_equal array_id, numbers.object_id + end + + def test_extract_on_empty_array + empty_array = [] + array_id = empty_array.object_id + + new_empty_array = empty_array.extract! { } + + assert_equal [], new_empty_array + assert_equal [], empty_array + assert_equal array_id, empty_array.object_id + end +end diff --git a/activesupport/test/core_ext/date_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb index 266829a452..58a24b60b6 100644 --- a/activesupport/test/core_ext/date_and_time_compatibility_test.rb +++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb @@ -248,7 +248,7 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_string_to_time_frozen_preserves_timezone with_preserve_timezone(true) do with_env_tz "US/Eastern" do - source = "2016-04-23T15:11:12+01:00".freeze + source = "2016-04-23T15:11:12+01:00" time = source.to_time assert_instance_of Time, time @@ -262,7 +262,7 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_string_to_time_frozen_does_not_preserve_time_zone with_preserve_timezone(false) do with_env_tz "US/Eastern" do - source = "2016-04-23T15:11:12+01:00".freeze + source = "2016-04-23T15:11:12+01:00" time = source.to_time assert_instance_of Time, time diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 894fb80cba..f9f6b21c9b 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -152,8 +152,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600) assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2) assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25) - assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) - assert_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) + assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) + assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) end def test_change diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb index 374114c11b..38fd60463d 100644 --- a/activesupport/test/core_ext/module/concerning_test.rb +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -5,7 +5,7 @@ require "active_support/core_ext/module/concerning" class ModuleConcerningTest < ActiveSupport::TestCase def test_concerning_declares_a_concern_and_includes_it_immediately - klass = Class.new { concerning(:Foo) {} } + klass = Class.new { concerning(:Foo) { } } assert_includes klass.ancestors, klass::Foo, klass.ancestors.inspect end end diff --git a/activesupport/test/core_ext/module/introspection_test.rb b/activesupport/test/core_ext/module/introspection_test.rb index 76d3012239..d8409d5e44 100644 --- a/activesupport/test/core_ext/module/introspection_test.rb +++ b/activesupport/test/core_ext/module/introspection_test.rb @@ -15,25 +15,43 @@ module ParentA end class IntrospectionTest < ActiveSupport::TestCase + def test_module_parent_name + assert_equal "ParentA", ParentA::B.module_parent_name + assert_equal "ParentA::B", ParentA::B::C.module_parent_name + assert_nil ParentA.module_parent_name + end + + def test_module_parent_name_when_frozen + assert_equal "ParentA", ParentA::FrozenB.module_parent_name + assert_equal "ParentA::B", ParentA::B::FrozenC.module_parent_name + end + def test_parent_name - assert_equal "ParentA", ParentA::B.parent_name - assert_equal "ParentA::B", ParentA::B::C.parent_name - assert_nil ParentA.parent_name + assert_deprecated do + assert_equal "ParentA", ParentA::B.parent_name + end end - def test_parent_name_when_frozen - assert_equal "ParentA", ParentA::FrozenB.parent_name - assert_equal "ParentA::B", ParentA::B::FrozenC.parent_name + def test_module_parent + assert_equal ParentA::B, ParentA::B::C.module_parent + assert_equal ParentA, ParentA::B.module_parent + assert_equal Object, ParentA.module_parent end def test_parent - assert_equal ParentA::B, ParentA::B::C.parent - assert_equal ParentA, ParentA::B.parent - assert_equal Object, ParentA.parent + assert_deprecated do + assert_equal ParentA, ParentA::B.parent + end + end + + def test_module_parents + assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.module_parents + assert_equal [ParentA, Object], ParentA::B.module_parents end def test_parents - assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.parents - assert_equal [ParentA, Object], ParentA::B.parents + assert_deprecated do + assert_equal [ParentA, Object], ParentA::B.parents + end end end diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb index a3d8daab5b..9052d209a3 100644 --- a/activesupport/test/core_ext/object/instance_variables_test.rb +++ b/activesupport/test/core_ext/object/instance_variables_test.rb @@ -19,15 +19,15 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase end def test_instance_exec_passes_arguments_to_block - assert_equal %w(hello goodbye), "hello".dup.instance_exec("goodbye") { |v| [self, v] } + assert_equal %w(hello goodbye), (+"hello").instance_exec("goodbye") { |v| [self, v] } end def test_instance_exec_with_frozen_obj - assert_equal %w(olleh goodbye), "hello".freeze.instance_exec("goodbye") { |v| [reverse, v] } + assert_equal %w(olleh goodbye), "hello".instance_exec("goodbye") { |v| [reverse, v] } end def test_instance_exec_nested - assert_equal %w(goodbye olleh bar), "hello".dup.instance_exec("goodbye") { |arg| + assert_equal %w(goodbye olleh bar), (+"hello").instance_exec("goodbye") { |arg| [arg] + instance_exec("bar") { |v| [reverse, v] } } end end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index b0b7ef0913..561dadbbcf 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -88,7 +88,7 @@ class ToQueryTest < ActiveSupport::TestCase } expected = "foo[contents][][name]=gorby&foo[contents][][id]=123&foo[contents][][name]=puff&foo[contents][][d]=true" - assert_equal expected, URI.decode(params.to_query) + assert_equal expected, URI.decode_www_form_component(params.to_query) end private diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 7c7a78f461..4b8efb8a93 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -108,14 +108,14 @@ class RangeTest < ActiveSupport::TestCase def test_each_on_time_with_zone twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30)) assert_raises TypeError do - ((twz - 1.hour)..twz).each {} + ((twz - 1.hour)..twz).each { } end end def test_step_on_time_with_zone twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30)) assert_raises TypeError do - ((twz - 1.hour)..twz).step(1) {} + ((twz - 1.hour)..twz).step(1) { } end end @@ -131,11 +131,11 @@ class RangeTest < ActiveSupport::TestCase def test_date_time_with_each datetime = DateTime.now - assert(((datetime - 1.hour)..datetime).each {}) + assert(((datetime - 1.hour)..datetime).each { }) end def test_date_time_with_step datetime = DateTime.now - assert(((datetime - 1.hour)..datetime).step(1) {}) + assert(((datetime - 1.hour)..datetime).step(1) { }) end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index b8de16cc5e..2468fe3603 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -25,7 +25,7 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_strip_heredoc_on_a_frozen_string - assert "".freeze.strip_heredoc.frozen? + assert "".strip_heredoc.frozen? end def test_strip_heredoc_on_a_string_with_no_lines @@ -245,8 +245,8 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_string_squish - original = %{\u205f\u3000 A string surrounded by various unicode spaces, - with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007}.dup + original = +%{\u205f\u3000 A string surrounded by various unicode spaces, + with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007} expected = "A string surrounded by various unicode spaces, " \ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." @@ -378,8 +378,8 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_truncate_multibyte - assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".dup.force_encoding(Encoding::UTF_8), - "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".dup.force_encoding(Encoding::UTF_8).truncate(10) + assert_equal (+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...").force_encoding(Encoding::UTF_8), + (+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244").force_encoding(Encoding::UTF_8).truncate(10) end def test_truncate_should_not_be_html_safe @@ -400,7 +400,7 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_remove! - original = "This is a very good day to die".dup + original = +"This is a very good day to die" assert_equal "This is a good day to die", original.remove!(" very") assert_equal "This is a good day to die", original assert_equal "This is a good day", original.remove!(" to ", /die/) @@ -469,6 +469,15 @@ class StringAccessTest < ActiveSupport::TestCase assert_not_same different_string, string end + test "#first with negative Integer is deprecated" do + string = "hello" + message = "Calling String#first with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + assert_deprecated(message) do + string.first(-1) + end + end + test "#last returns the last character" do assert_equal "o", "hello".last assert_equal "x", "x".last @@ -487,6 +496,15 @@ class StringAccessTest < ActiveSupport::TestCase assert_not_same different_string, string end + test "#last with negative Integer is deprecated" do + string = "hello" + message = "Calling String#last with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + assert_deprecated(message) do + string.last(-1) + end + end + test "access returns a real string" do hash = {} hash["h"] = true @@ -733,7 +751,7 @@ end class OutputSafetyTest < ActiveSupport::TestCase def setup - @string = "hello".dup + @string = +"hello" @object = Class.new(Object) do def to_s "other" @@ -809,7 +827,7 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "Concatting safe onto unsafe yields unsafe" do - @other_string = "other".dup + @other_string = +"other" string = @string.html_safe @other_string.concat(string) @@ -832,7 +850,7 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "Concatting safe onto unsafe with << yields unsafe" do - @other_string = "other".dup + @other_string = +"other" string = @string.html_safe @other_string << string @@ -888,7 +906,55 @@ class OutputSafetyTest < ActiveSupport::TestCase test "Concatting an integer to safe always yields safe" do string = @string.html_safe string = string.concat(13) - assert_equal "hello".dup.concat(13), string + assert_equal (+"hello").concat(13), string + assert_predicate string, :html_safe? + end + + test "Inserting safe into safe yields safe" do + string = "foo".html_safe + string.insert(0, "<b>".html_safe) + + assert_equal "<b>foo", string + assert_predicate string, :html_safe? + end + + test "Inserting unsafe into safe yields escaped safe" do + string = "foo".html_safe + string.insert(0, "<b>") + + assert_equal "<b>foo", string + assert_predicate string, :html_safe? + end + + test "Replacing safe with safe yields safe" do + string = "foo".html_safe + string.replace("<b>".html_safe) + + assert_equal "<b>", string + assert_predicate string, :html_safe? + end + + test "Replacing safe with unsafe yields escaped safe" do + string = "foo".html_safe + string.replace("<b>") + + assert_equal "<b>", string + assert_predicate string, :html_safe? + end + + test "Replacing index of safe with safe yields safe" do + string = "foo".html_safe + string[0] = "<b>".html_safe + + assert_equal "<b>oo", string + assert_predicate string, :html_safe? + end + + test "Replacing index of safe with unsafe yields escaped safe" do + string = "foo".html_safe + string[0] = "<b>" + + assert_equal "<b>oo", string assert_predicate string, :html_safe? end diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index e1cb22fda8..7078f3506d 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -950,39 +950,39 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end class TimeExtMarshalingTest < ActiveSupport::TestCase - def test_marshaling_with_utc_instance + def test_marshalling_with_utc_instance t = Time.utc(2000) - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal "UTC", unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal "UTC", unmarshalled.zone + assert_equal t, unmarshalled end - def test_marshaling_with_local_instance + def test_marshalling_with_local_instance t = Time.local(2000) - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal t.zone, unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal t.zone, unmarshalled.zone + assert_equal t, unmarshalled end - def test_marshaling_with_frozen_utc_instance + def test_marshalling_with_frozen_utc_instance t = Time.utc(2000).freeze - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal "UTC", unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal "UTC", unmarshalled.zone + assert_equal t, unmarshalled end - def test_marshaling_with_frozen_local_instance + def test_marshalling_with_frozen_local_instance t = Time.local(2000).freeze - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal t.zone, unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal t.zone, unmarshalled.zone + assert_equal t, unmarshalled end def test_marshalling_preserves_fractional_seconds t = Time.parse("00:00:00.500") - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal t.to_f, unmarshaled.to_f - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal t.to_f, unmarshalled.to_f + assert_equal t, unmarshalled end def test_last_quarter_on_31st diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index e650209268..f6e836e446 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1105,7 +1105,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_use_zone_raises_on_invalid_timezone Time.zone = "Alaska" assert_raise ArgumentError do - Time.use_zone("No such timezone exists") {} + Time.use_zone("No such timezone exists") { } end assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone end diff --git a/activesupport/test/dependencies/module_folder/lib_class.rb b/activesupport/test/dependencies/module_folder/lib_class.rb new file mode 100644 index 0000000000..c6b52610c1 --- /dev/null +++ b/activesupport/test/dependencies/module_folder/lib_class.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +ConstFromLib = 1 + +module ModuleFolder + class LibClass + end +end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 84cb64a7c2..9f2755a25c 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -282,6 +282,32 @@ class DependenciesTest < ActiveSupport::TestCase remove_constants(:ModuleFolder) end + def test_module_with_nested_class_requiring_lib_class + with_autoloading_fixtures do + ModuleFolder::NestedWithRequire + + assert defined?(ModuleFolder::LibClass) + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass") + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ConstFromLib") + end + ensure + remove_constants(:ModuleFolder) + remove_constants(:ConstFromLib) + end + + def test_module_with_nested_class_and_parent_requiring_lib_class + with_autoloading_fixtures do + NestedWithRequireParent + + assert defined?(ModuleFolder::LibClass) + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass") + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ConstFromLib") + end + ensure + remove_constants(:ModuleFolder) + remove_constants(:ConstFromLib) + end + def test_directories_may_manifest_as_nested_classes with_autoloading_fixtures do assert_kind_of Class, ClassFolder @@ -816,7 +842,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_new_contants_in_without_constants - assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) {}) + assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) { }) assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } end @@ -892,7 +918,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_new_constants_in_with_illegal_module_name_raises_correct_error assert_raise(NameError) do - ActiveSupport::Dependencies.new_constants_in("Illegal-Name") {} + ActiveSupport::Dependencies.new_constants_in("Illegal-Name") { } end end @@ -1130,3 +1156,52 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.hook! end end + +class DependenciesLogging < ActiveSupport::TestCase + MESSAGE = "message" + + def with_settings(logger, verbose) + original_logger = ActiveSupport::Dependencies.logger + original_verbose = ActiveSupport::Dependencies.verbose + + ActiveSupport::Dependencies.logger = logger + ActiveSupport::Dependencies.verbose = verbose + + yield + ensure + ActiveSupport::Dependencies.logger = original_logger + ActiveSupport::Dependencies.verbose = original_verbose + end + + def fake_logger + Class.new do + def self.debug(message) + message + end + end + end + + test "does not log if the logger is nil and verbose is false" do + with_settings(nil, false) do + assert_nil ActiveSupport::Dependencies.log(MESSAGE) + end + end + + test "does not log if the logger is nil and verbose is true" do + with_settings(nil, true) do + assert_nil ActiveSupport::Dependencies.log(MESSAGE) + end + end + + test "does not log if the logger is set and verbose is false" do + with_settings(fake_logger, false) do + assert_nil ActiveSupport::Dependencies.log(MESSAGE) + end + end + + test "logs if the logger is set and verbose is true" do + with_settings(fake_logger, true) do + assert_equal "autoloading: #{MESSAGE}", ActiveSupport::Dependencies.log(MESSAGE) + end + end +end diff --git a/activesupport/test/encrypted_configuration_test.rb b/activesupport/test/encrypted_configuration_test.rb index 93ccf457de..387d6e1c1f 100644 --- a/activesupport/test/encrypted_configuration_test.rb +++ b/activesupport/test/encrypted_configuration_test.rb @@ -42,6 +42,12 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase assert @credentials.something[:good] end + test "reading comment-only configuration" do + @credentials.write("# comment") + + assert_equal @credentials.config, {} + end + test "change configuration by key file" do @credentials.write({ something: { good: true } }.to_yaml) @credentials.change do |config_file| diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index d3af0dbef3..a557608986 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -38,7 +38,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase FileUtils.touch(tmpfiles) - checker = new_checker(tmpfiles) {} + checker = new_checker(tmpfiles) { } assert_not_predicate checker, :updated? # Pipes used for flow control across fork. diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb index af441064dd..3026f002c3 100644 --- a/activesupport/test/executor_test.rb +++ b/activesupport/test/executor_test.rb @@ -23,7 +23,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.to_run { @foo = true } executor.to_complete { result = @foo } - executor.wrap {} + executor.wrap { } assert result end @@ -85,7 +85,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) - executor.wrap {} + executor.wrap { } assert_equal :some_state, supplied_state end @@ -105,7 +105,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) - executor.wrap {} + executor.wrap { } assert_nil supplied_state end @@ -129,7 +129,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) assert_raises(DummyError) do - executor.wrap {} + executor.wrap { } end assert_equal :none, supplied_state @@ -154,7 +154,7 @@ class ExecutorTest < ActiveSupport::TestCase end assert_raises(DummyError) do - executor.wrap {} + executor.wrap { } end assert_equal :some_state, supplied_state @@ -187,7 +187,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook_class.new(:c), outer: true) executor.register_hook(hook_class.new(:d)) - executor.wrap {} + executor.wrap { } assert_equal [:run_c, :run_a, :run_b, :run_d, :complete_a, :complete_b, :complete_d, :complete_c], invoked assert_equal [:state_a, :state_b, :state_d, :state_c], supplied_state @@ -209,9 +209,9 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) before = RubyVM.stat(:class_serial) - executor.wrap {} - executor.wrap {} - executor.wrap {} + executor.wrap { } + executor.wrap { } + executor.wrap { } after = RubyVM.stat(:class_serial) assert_equal before, after diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index eebff18ef1..f81e0dc70f 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -57,6 +57,13 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys end + def test_to_options_for_hash_with_indifferent_access + assert_instance_of Hash, @symbols.with_indifferent_access.to_options + assert_equal @symbols, @symbols.with_indifferent_access.to_options + assert_equal @symbols, @strings.with_indifferent_access.to_options + assert_equal @symbols, @mixed.with_indifferent_access.to_options + end + def test_deep_symbolize_keys_for_hash_with_indifferent_access assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys @@ -672,6 +679,17 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal "bender", slice["login"] end + def test_indifferent_without + original = { a: "x", b: "y", c: 10 }.with_indifferent_access + expected = { c: 10 }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + # Should return a new hash without the given keys. + assert_equal expected, original.without(*keys), keys.inspect + assert_not_equal expected, original + end + end + def test_indifferent_extract original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access expected = { a: 1, b: 2 }.with_indifferent_access diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index ebc5df14dd..8062873386 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -157,6 +157,16 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal({ "foo" => "hello" }, JSON.parse(json)) end + def test_struct_to_json_with_options_nested + klass = Struct.new(:foo, :bar) + struct = klass.new "hello", "world" + parent_struct = klass.new struct, "world" + json = parent_struct.to_json only: [:foo] + + assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json)) + end + + def test_hash_should_pass_encoding_options_to_children_in_as_json person = { name: "John", diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb index cdde2c573a..9dfc0b2154 100644 --- a/activesupport/test/key_generator_test.rb +++ b/activesupport/test/key_generator_test.rb @@ -9,9 +9,6 @@ rescue LoadError, NameError $stderr.puts "Skipping KeyGenerator test: broken OpenSSL install" else - require "active_support/time" - require "active_support/json" - class KeyGeneratorTest < ActiveSupport::TestCase def setup @secret = SecureRandom.hex(64) diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb index 773b0daa1d..160e1156b6 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -40,7 +40,7 @@ class LoggerTest < ActiveSupport::TestCase logger = Logger.new f logger.level = Logger::DEBUG - str = "\x80".dup + str = +"\x80" str.force_encoding("ASCII-8BIT") logger.add Logger::DEBUG, str @@ -58,7 +58,7 @@ class LoggerTest < ActiveSupport::TestCase logger = Logger.new f logger.level = Logger::DEBUG - str = "\x80".dup + str = +"\x80" str.force_encoding("ASCII-8BIT") logger.add Logger::DEBUG, str diff --git a/activesupport/test/metadata/shared_metadata_tests.rb b/activesupport/test/metadata/shared_metadata_tests.rb index 08bb0c648e..cf571223e5 100644 --- a/activesupport/test/metadata/shared_metadata_tests.rb +++ b/activesupport/test/metadata/shared_metadata_tests.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true module SharedMessageMetadataTests - def teardown - travel_back - super - end - def null_serializing? false end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 061446c782..f87099566b 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -53,7 +53,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_forwarded_method_with_non_string_result_should_be_returned_verbatim - str = "".dup + str = +"" str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @@ -61,26 +61,32 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_should_concatenate - mb_a = "a".dup.mb_chars - mb_b = "b".dup.mb_chars + mb_a = (+"a").mb_chars + mb_b = (+"b").mb_chars assert_equal "ab", mb_a + "b" assert_equal "ab", "a" + mb_b assert_equal "ab", mb_a + mb_b assert_equal "ab", mb_a << "b" - assert_equal "ab", "a".dup << mb_b + assert_equal "ab", (+"a") << mb_b assert_equal "abb", mb_a << mb_b end def test_consumes_utf8_strings - assert @proxy_class.consumes?(UNICODE_STRING) - assert @proxy_class.consumes?(ASCII_STRING) - assert_not @proxy_class.consumes?(BYTE_STRING) + ActiveSupport::Deprecation.silence do + assert @proxy_class.consumes?(UNICODE_STRING) + assert @proxy_class.consumes?(ASCII_STRING) + assert_not @proxy_class.consumes?(BYTE_STRING) + end + end + + def test_consumes_is_deprecated + assert_deprecated { @proxy_class.consumes?(UNICODE_STRING) } end def test_concatenation_should_return_a_proxy_class_instance assert_equal ActiveSupport::Multibyte.proxy_class, ("a".mb_chars + "b").class - assert_equal ActiveSupport::Multibyte.proxy_class, ("a".dup.mb_chars << "b").class + assert_equal ActiveSupport::Multibyte.proxy_class, ((+"a").mb_chars << "b").class end def test_ascii_strings_are_treated_at_utf8_strings @@ -90,8 +96,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase def test_concatenate_should_return_proxy_instance assert(("a".mb_chars + "b").kind_of?(@proxy_class)) assert(("a".mb_chars + "b".mb_chars).kind_of?(@proxy_class)) - assert(("a".dup.mb_chars << "b").kind_of?(@proxy_class)) - assert(("a".dup.mb_chars << "b".mb_chars).kind_of?(@proxy_class)) + assert(((+"a").mb_chars << "b").kind_of?(@proxy_class)) + assert(((+"a").mb_chars << "b".mb_chars).kind_of?(@proxy_class)) end def test_should_return_string_as_json @@ -135,7 +141,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_tidy_bytes_bang_should_change_wrapped_string - original = " Un bUen café \x92".dup + original = +" Un bUen café \x92" proxy = chars(original.dup) proxy.tidy_bytes! assert_not_equal original, proxy.to_s @@ -152,7 +158,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_string_methods_are_chainable - assert chars("".dup).insert(0, "").kind_of?(ActiveSupport::Multibyte.proxy_class) + assert chars(+"").insert(0, "").kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").rjust(1).kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").ljust(1).kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").center(1).kind_of?(ActiveSupport::Multibyte.proxy_class) @@ -165,7 +171,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class) - assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) + ActiveSupport::Deprecation.silence do + assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) + end assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class) @@ -197,7 +205,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_should_use_character_offsets_for_insert_offsets - assert_equal "", "".dup.mb_chars.insert(0, "") + assert_equal "", (+"").mb_chars.insert(0, "") assert_equal "こわにちわ", @chars.insert(1, "わ") assert_equal "こわわわにちわ", @chars.insert(2, "わわ") assert_equal "わこわわわにちわ", @chars.insert(0, "わ") @@ -383,10 +391,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_reverse_should_work_with_normalized_strings str = "bös" reversed_str = "söb" - assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse - assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse - assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse - assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse + ActiveSupport::Deprecation.silence do + assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse + assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse + assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse + assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse + end assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse assert_equal chars(reversed_str).compose, chars(str).compose.reverse end @@ -420,13 +430,13 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_slice_bang_removes_the_slice_from_the_receiver - chars = "úüù".dup.mb_chars + chars = (+"úüù").mb_chars chars.slice!(0, 2) assert_equal "ù", chars end def test_slice_bang_returns_nil_and_does_not_modify_receiver_if_out_of_bounds - string = "úüù".dup + string = +"úüù" chars = string.mb_chars assert_nil chars.slice!(4, 5) assert_equal "úüù", chars @@ -477,7 +487,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_method_works_for_proxyed_methods assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars - chars = "hello".mb_chars + chars = +"hello".mb_chars assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars assert_equal "Hello", chars assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String @@ -568,7 +578,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase def test_composition_exclusion_is_set_up_properly # Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly qa = [0x915, 0x93c].pack("U*") - assert_equal qa, chars(qa).normalize(:c) + ActiveSupport::Deprecation.silence do + assert_equal qa, chars(qa).normalize(:c) + end end # Test for the Public Review Issue #29, bad explanation of composition might lead to a @@ -578,17 +590,21 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase [0x0B47, 0x0300, 0x0B3E], [0x1100, 0x0300, 0x1161] ].map { |c| c.pack("U*") }.each do |c| - assert_equal_codepoints c, chars(c).normalize(:c) + ActiveSupport::Deprecation.silence do + assert_equal_codepoints c, chars(c).normalize(:c) + end end end def test_normalization_shouldnt_strip_null_bytes null_byte_str = "Test\0test" - assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) - assert_equal null_byte_str, chars(null_byte_str).normalize(:c) - assert_equal null_byte_str, chars(null_byte_str).normalize(:d) - assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) + ActiveSupport::Deprecation.silence do + assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) + assert_equal null_byte_str, chars(null_byte_str).normalize(:c) + assert_equal null_byte_str, chars(null_byte_str).normalize(:d) + assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) + end assert_equal null_byte_str, chars(null_byte_str).decompose assert_equal null_byte_str, chars(null_byte_str).compose end @@ -601,11 +617,13 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase 323 # COMBINING DOT BELOW ].pack("U*") - assert_equal_codepoints "", chars("").normalize - assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s - assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s - assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s - assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s + ActiveSupport::Deprecation.silence do + assert_equal_codepoints "", chars("").normalize + assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s + assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s + assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s + assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s + end end def test_should_compute_grapheme_length @@ -719,6 +737,41 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars end + def test_unicode_normalize_deprecation + # String#unicode_normalize default form is `:nfc`, and + # different than Multibyte::Unicode default, `:nkfc`. + # Deprecation should suggest the right form if no params + # are given and default is used. + assert_deprecated(/unicode_normalize\(:nfkc\)/) do + ActiveSupport::Multibyte::Unicode.normalize("") + end + + assert_deprecated(/unicode_normalize\(:nfd\)/) do + ActiveSupport::Multibyte::Unicode.normalize("", :d) + end + end + + def test_chars_normalize_deprecation + # String#unicode_normalize default form is `:nfc`, and + # different than Multibyte::Unicode default, `:nkfc`. + # Deprecation should suggest the right form if no params + # are given and default is used. + assert_deprecated(/unicode_normalize\(:nfkc\)/) do + "".mb_chars.normalize + end + + assert_deprecated(/unicode_normalize\(:nfc\)/) { "".mb_chars.normalize(:c) } + assert_deprecated(/unicode_normalize\(:nfd\)/) { "".mb_chars.normalize(:d) } + assert_deprecated(/unicode_normalize\(:nfkc\)/) { "".mb_chars.normalize(:kc) } + assert_deprecated(/unicode_normalize\(:nfkd\)/) { "".mb_chars.normalize(:kd) } + end + + def test_unicode_deprecations + assert_deprecated { ActiveSupport::Multibyte::Unicode.downcase("") } + assert_deprecated { ActiveSupport::Multibyte::Unicode.upcase("") } + assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") } + end + private def string_from_classes(classes) @@ -732,21 +785,3 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase end.pack("U*") end end - -class MultibyteInternalsTest < ActiveSupport::TestCase - include MultibyteTestHelpers - - test "Chars translates a character offset to a byte offset" do - example = chars("Puisque c'était son erreur, il m'a aidé") - [ - [0, 0], - [3, 3], - [12, 11], - [14, 13], - [41, 39] - ].each do |byte_offset, character_offset| - assert_equal character_offset, example.send(:translate_offset, byte_offset), - "Expected byte offset #{byte_offset} to translate to #{character_offset}" - end - end -end diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index a704505fc6..b19689723f 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -18,64 +18,72 @@ class MultibyteConformanceTest < ActiveSupport::TestCase end def test_normalizations_C - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols - # CONFORMANCE: - # 1. The following invariants must be true for all conformant implementations - # - # NFC - # c2 == NFC(c1) == NFC(c2) == NFC(c3) - assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" - # - # c4 == NFC(c4) == NFC(c5) - assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end end end def test_normalizations_D - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols - # - # NFD - # c3 == NFD(c1) == NFD(c2) == NFD(c3) - assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" - # c5 == NFD(c4) == NFD(c5) - assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end end end def test_normalizations_KC - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKC - # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) - assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end end end def test_normalizations_KD - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKD - # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) - assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end end end diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb index 61b171a8d4..97963279af 100644 --- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -17,10 +17,12 @@ class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase end def test_breaks - each_line_of_break_tests do |*cols| - *clusters, comment = *cols - packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) - assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + ActiveSupport::Deprecation.silence do + each_line_of_break_tests do |*cols| + *clusters, comment = *cols + packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) + assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + end end end diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb index 3674ea44f3..82edf69294 100644 --- a/activesupport/test/multibyte_normalization_conformance_test.rb +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -18,64 +18,72 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase end def test_normalizations_C - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols - # CONFORMANCE: - # 1. The following invariants must be true for all conformant implementations - # - # NFC - # c2 == NFC(c1) == NFC(c2) == NFC(c3) - assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" - # - # c4 == NFC(c4) == NFC(c5) - assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end end end def test_normalizations_D - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols - # - # NFD - # c3 == NFD(c1) == NFD(c2) == NFD(c3) - assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" - # c5 == NFD(c4) == NFD(c5) - assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end end end def test_normalizations_KC - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKC - # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) - assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end end end def test_normalizations_KD - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKD - # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) - assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end end end diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 98f36aa939..7565655f25 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -27,9 +27,9 @@ module MultibyteTestHelpers CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}" FileUtils.mkdir_p(CACHE_DIR) - UNICODE_STRING = "こにちわ".freeze - ASCII_STRING = "ohayo".freeze - BYTE_STRING = "\270\236\010\210\245".dup.force_encoding("ASCII-8BIT").freeze + UNICODE_STRING = "こにちわ" + ASCII_STRING = "ohayo" + BYTE_STRING = (+"\270\236\010\210\245").force_encoding("ASCII-8BIT").freeze def chars(str) ActiveSupport::Multibyte::Chars.new(str) diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 62817a839a..54fd4345fb 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -90,7 +90,7 @@ module Notifications ActiveSupport::Notifications.subscribe("foo", TestSubscriber.new) ActiveSupport::Notifications.instrument("foo") do - ActiveSupport::Notifications.subscribe("foo") {} + ActiveSupport::Notifications.subscribe("foo") { } end ensure ActiveSupport::Notifications.notifier = old_notifier diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb new file mode 100644 index 0000000000..d2dc71061d --- /dev/null +++ b/activesupport/test/parameter_filter_test.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/hash" +require "active_support/parameter_filter" + +class ParameterFilterTest < ActiveSupport::TestCase + test "process parameter filter" do + test_hashes = [ + [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], + [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'], + [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'], + [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'], + [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'], + [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'], + [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], + [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]] + + test_hashes.each do |before_filter, after_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + assert_equal after_filter, parameter_filter.filter(before_filter) + + filter_words << "blah" + filter_words << lambda { |key, value| + value.reverse! if key =~ /bargain/ + } + filter_words << lambda { |key, value, original_params| + value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello" + } + + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } } + after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } } + + assert_equal after_filter, parameter_filter.filter(before_filter) + end + end + + test "filter should return mask option when value is filtered" do + mask = Object.new.freeze + test_hashes = [ + [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], + [{ "foo" => "bar" }, { "foo" => mask }, %w'foo'], + [{ "foo" => "bar", "bar" => "foo" }, { "foo" => mask, "bar" => "foo" }, %w'foo baz'], + [{ "foo" => "bar", "baz" => "foo" }, { "foo" => mask, "baz" => mask }, %w'foo baz'], + [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => mask, "bar" => "foo" } }, %w'fo'], + [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => mask }, %w'f banana'], + [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => mask, "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], + [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => mask }, "1"] }, [/foo/]]] + + test_hashes.each do |before_filter, after_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask) + assert_equal after_filter, parameter_filter.filter(before_filter) + + filter_words << "blah" + filter_words << lambda { |key, value| + value.reverse! if key =~ /bargain/ + } + filter_words << lambda { |key, value, original_params| + value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello" + } + + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask) + before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } } + after_filter["barg"] = { :bargain => "niag", "blah" => mask, "bar" => { "bargain" => { "blah" => mask, "hello" => "world!" } } } + + assert_equal after_filter, parameter_filter.filter(before_filter) + end + end + + test "filter_param" do + parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/]) + assert_equal "[FILTERED]", parameter_filter.filter_param("food", "secret vlaue") + assert_equal "[FILTERED]", parameter_filter.filter_param("baz.foo", "secret vlaue") + assert_equal "[FILTERED]", parameter_filter.filter_param("barbar", "secret vlaue") + assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value") + end + + test "filter_param can work with empty filters" do + parameter_filter = ActiveSupport::ParameterFilter.new + assert_equal "bar", parameter_filter.filter_param("foo", "bar") + end + + test "parameter filter should maintain hash with indifferent access" do + test_hashes = [ + [{ "foo" => "bar" }.with_indifferent_access, ["blah"]], + [{ "foo" => "bar" }.with_indifferent_access, []] + ] + + test_hashes.each do |before_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, + parameter_filter.filter(before_filter) + end + end + + test "filter_param should return mask option when value is filtered" do + mask = Object.new.freeze + parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/], mask: mask) + assert_equal mask, parameter_filter.filter_param("food", "secret vlaue") + assert_equal mask, parameter_filter.filter_param("baz.foo", "secret vlaue") + assert_equal mask, parameter_filter.filter_param("barbar", "secret vlaue") + assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value") + end +end diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb index 976917c1a1..1b7cc253d9 100644 --- a/activesupport/test/reloader_test.rb +++ b/activesupport/test/reloader_test.rb @@ -35,13 +35,13 @@ class ReloaderTest < ActiveSupport::TestCase r = new_reloader { true } invoked = false r.to_run { invoked = true } - r.wrap {} + r.wrap { } assert invoked r = new_reloader { false } invoked = false r.to_run { invoked = true } - r.wrap {} + r.wrap { } assert_not invoked end @@ -53,7 +53,7 @@ class ReloaderTest < ActiveSupport::TestCase reloader.executor.to_run { called << :executor_run } reloader.executor.to_complete { called << :executor_complete } - reloader.wrap {} + reloader.wrap { } assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete], called called = [] @@ -63,7 +63,7 @@ class ReloaderTest < ActiveSupport::TestCase reloader.check = lambda { false } called = [] - reloader.wrap {} + reloader.wrap { } assert_equal [:executor_run, :executor_complete], called called = [] diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 9456bb8753..49a3951623 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -75,16 +75,41 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal "my_test", str end - test "Should not return safe buffer from gsub" do - altered_buffer = @buffer.gsub("", "asdf") - assert_equal "asdf", altered_buffer - assert_not_predicate altered_buffer, :html_safe? - end + { + capitalize: nil, + chomp: nil, + chop: nil, + delete: "foo", + delete_prefix: "foo", + delete_suffix: "foo", + downcase: nil, + gsub: ["foo", "bar"], + lstrip: nil, + next: nil, + reverse: nil, + rstrip: nil, + slice: "foo", + squeeze: nil, + strip: nil, + sub: ["foo", "bar"], + succ: nil, + swapcase: nil, + tr: ["foo", "bar"], + tr_s: ["foo", "bar"], + unicode_normalize: nil, + upcase: nil, + }.each do |unsafe_method, dummy_args| + test "Should not return safe buffer from #{unsafe_method}" do + skip unless String.method_defined?(unsafe_method) + altered_buffer = @buffer.send(unsafe_method, *dummy_args) + assert_not_predicate altered_buffer, :html_safe? + end - test "Should not return safe buffer from gsub!" do - @buffer.gsub!("", "asdf") - assert_equal "asdf", @buffer - assert_not_predicate @buffer, :html_safe? + test "Should not return safe buffer from #{unsafe_method}!" do + skip unless String.method_defined?("#{unsafe_method}!") + @buffer.send("#{unsafe_method}!", *dummy_args) + assert_not_predicate @buffer, :html_safe? + end end test "Should escape dirty buffers on add" do @@ -150,6 +175,18 @@ class SafeBufferTest < ActiveSupport::TestCase assert_not y.html_safe?, "should not be safe" end + test "Should continue safe on slice" do + x = "<div>foo</div>".html_safe + + assert_predicate x, :html_safe? + + # getting a slice of it + y = x[0..-1] + + # should still be safe + assert_predicate y, :html_safe? + end + test "Should work with interpolation (array argument)" do x = "foo %s bar".html_safe % ["qux"] assert_equal "foo qux bar", x diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb index 42fd5eefc1..34479020e1 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -11,29 +11,29 @@ class ShareLockTest < ActiveSupport::TestCase def test_reentrancy thread = Thread.new do - @lock.sharing { @lock.sharing {} } - @lock.exclusive { @lock.exclusive {} } + @lock.sharing { @lock.sharing { } } + @lock.exclusive { @lock.exclusive { } } end assert_threads_not_stuck thread end def test_sharing_doesnt_block with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_latch| - assert_threads_not_stuck(Thread.new { @lock.sharing {} }) + assert_threads_not_stuck(Thread.new { @lock.sharing { } }) end end def test_sharing_blocks_exclusive with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| @lock.exclusive(no_wait: true) { flunk } # polling should fail - exclusive_thread = Thread.new { @lock.exclusive {} } + exclusive_thread = Thread.new { @lock.exclusive { } } assert_threads_stuck_but_releasable_by_latch exclusive_thread, sharing_thread_release_latch end end def test_exclusive_blocks_sharing with_thread_waiting_in_lock_section(:exclusive) do |exclusive_thread_release_latch| - sharing_thread = Thread.new { @lock.sharing {} } + sharing_thread = Thread.new { @lock.sharing { } } assert_threads_stuck_but_releasable_by_latch sharing_thread, exclusive_thread_release_latch end end @@ -42,7 +42,7 @@ class ShareLockTest < ActiveSupport::TestCase with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| exclusive_threads = (1..2).map do Thread.new do - @lock.exclusive {} + @lock.exclusive { } end end @@ -53,7 +53,7 @@ class ShareLockTest < ActiveSupport::TestCase def test_sharing_is_upgradeable_to_exclusive upgrading_thread = Thread.new do @lock.sharing do - @lock.exclusive {} + @lock.exclusive { } end end assert_threads_not_stuck upgrading_thread @@ -66,7 +66,7 @@ class ShareLockTest < ActiveSupport::TestCase upgrading_thread = Thread.new do @lock.sharing do in_sharing.count_down - @lock.exclusive {} + @lock.exclusive { } end end @@ -81,7 +81,7 @@ class ShareLockTest < ActiveSupport::TestCase exclusive_threads = (1..2).map do Thread.new do @lock.send(use_upgrading ? :sharing : :tap) do - @lock.exclusive(purpose: :load, compatible: [:load, :unload]) {} + @lock.exclusive(purpose: :load, compatible: [:load, :unload]) { } end end end @@ -95,7 +95,7 @@ class ShareLockTest < ActiveSupport::TestCase with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| thread = Thread.new do @lock.sharing do - @lock.exclusive {} + @lock.exclusive { } end end @@ -105,7 +105,7 @@ class ShareLockTest < ActiveSupport::TestCase sharing_thread_release_latch.count_down thread = Thread.new do - @lock.exclusive {} + @lock.exclusive { } end assert_threads_not_stuck thread @@ -121,13 +121,13 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.send(use_upgrading ? :sharing : :tap) do together.wait - @lock.exclusive(purpose: :red, compatible: [:green, :purple]) {} + @lock.exclusive(purpose: :red, compatible: [:green, :purple]) { } end end, Thread.new do @lock.send(use_upgrading ? :sharing : :tap) do together.wait - @lock.exclusive(purpose: :blue, compatible: [:green]) {} + @lock.exclusive(purpose: :blue, compatible: [:green]) { } end end ] @@ -138,7 +138,7 @@ class ShareLockTest < ActiveSupport::TestCase # a sharing block. While it's blocked, it holds no lock, so it # doesn't interfere with any other attempts. no_purpose_thread = Thread.new do - @lock.exclusive {} + @lock.exclusive { } end assert_threads_stuck no_purpose_thread @@ -147,7 +147,7 @@ class ShareLockTest < ActiveSupport::TestCase # lock, but as soon as that's released, it can run -- # regardless of whether those threads hold share locks. compatible_thread = Thread.new do - @lock.exclusive(purpose: :green, compatible: []) {} + @lock.exclusive(purpose: :green, compatible: []) { } end assert_threads_stuck compatible_thread @@ -231,7 +231,7 @@ class ShareLockTest < ActiveSupport::TestCase assert_threads_stuck waiting_exclusive late_share_attempt = Thread.new do - @lock.sharing {} + @lock.sharing { } end assert_threads_stuck late_share_attempt @@ -252,14 +252,14 @@ class ShareLockTest < ActiveSupport::TestCase @lock.sharing do ready.wait attempt_reentrancy.wait - @lock.sharing {} + @lock.sharing { } end end exclusive = Thread.new do @lock.sharing do ready.wait - @lock.exclusive {} + @lock.exclusive { } end end @@ -280,7 +280,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) {} + @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) { } done.wait end end @@ -297,7 +297,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } done.wait end end, @@ -323,7 +323,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } done.wait end end, @@ -352,7 +352,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } done.wait end end, @@ -386,7 +386,7 @@ class ShareLockTest < ActiveSupport::TestCase incompatible_thread = Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } end end @@ -418,7 +418,7 @@ class ShareLockTest < ActiveSupport::TestCase incompatible_thread = Thread.new do ready.wait - @lock.exclusive(purpose: :z) {} + @lock.exclusive(purpose: :z) { } end recursive_yield_shares_thread = Thread.new do @@ -427,7 +427,7 @@ class ShareLockTest < ActiveSupport::TestCase @lock.yield_shares(compatible: [:y]) do do_nesting.wait @lock.sharing do - @lock.yield_shares(compatible: [:x, :y]) {} + @lock.yield_shares(compatible: [:x, :y]) { } end after_nesting.wait end @@ -439,12 +439,12 @@ class ShareLockTest < ActiveSupport::TestCase assert_threads_stuck incompatible_thread compatible_thread = Thread.new do - @lock.exclusive(purpose: :y) {} + @lock.exclusive(purpose: :y) { } end assert_threads_not_stuck compatible_thread post_nesting_incompatible_thread = Thread.new do - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } end assert_threads_stuck post_nesting_incompatible_thread diff --git a/activesupport/test/silence_logger_test.rb b/activesupport/test/silence_logger_test.rb new file mode 100644 index 0000000000..bd0c6b7f86 --- /dev/null +++ b/activesupport/test/silence_logger_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/logger_silence" +require "logger" + +class LoggerSilenceTest < ActiveSupport::TestCase + class MyLogger < ::Logger + include ActiveSupport::LoggerSilence + end + + setup do + @io = StringIO.new + @logger = MyLogger.new(@io) + end + + test "#silence silences the log" do + @logger.silence(Logger::ERROR) do + @logger.info("Foo") + end + @io.rewind + + assert_empty @io.read + end + + test "#debug? is true when setting the temporary level to Logger::DEBUG" do + @logger.level = Logger::INFO + + @logger.silence(Logger::DEBUG) do + assert_predicate @logger, :debug? + end + + assert_predicate @logger, :info? + end +end diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index e2b41cf8ee..cff73472c3 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -19,9 +19,10 @@ class TaggedLoggingTest < ActiveSupport::TestCase test "sets logger.formatter if missing and extends it with a tagging API" do logger = Logger.new(StringIO.new) assert_nil logger.formatter - ActiveSupport::TaggedLogging.new(logger) - assert_not_nil logger.formatter - assert_respond_to logger.formatter, :tagged + + other_logger = ActiveSupport::TaggedLogging.new(logger) + assert_not_nil other_logger.formatter + assert_respond_to other_logger.formatter, :tagged end test "tagged once" do @@ -83,16 +84,28 @@ class TaggedLoggingTest < ActiveSupport::TestCase end test "keeps each tag in their own instance" do - @other_output = StringIO.new - @other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@other_output)) + other_output = StringIO.new + other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(other_output)) @logger.tagged("OMG") do - @other_logger.tagged("BCX") do + other_logger.tagged("BCX") do @logger.info "Cool story" - @other_logger.info "Funky time" + other_logger.info "Funky time" end end assert_equal "[OMG] Cool story\n", @output.string - assert_equal "[BCX] Funky time\n", @other_output.string + assert_equal "[BCX] Funky time\n", other_output.string + end + + test "does not share the same formatter instance of the original logger" do + other_logger = ActiveSupport::TaggedLogging.new(@logger) + + @logger.tagged("OMG") do + other_logger.tagged("BCX") do + @logger.info "Cool story" + other_logger.info "Funky time" + end + end + assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string end test "cleans up the taggings on flush" do diff --git a/activesupport/test/testing/method_call_assertions_test.rb b/activesupport/test/testing/method_call_assertions_test.rb index 0500e47def..669463bd31 100644 --- a/activesupport/test/testing/method_call_assertions_test.rb +++ b/activesupport/test/testing/method_call_assertions_test.rb @@ -36,6 +36,8 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase assert_called(@object, :increment, returns: 10) do assert_equal 10, @object.increment end + + assert_equal 1, @object.increment end def test_assert_called_failure @@ -58,18 +60,20 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase assert_match(/dang it.\nExpected increment/, error.message) end - def test_assert_called_with - assert_called_with(@object, :increment) do - @object.increment - end - end - def test_assert_called_with_arguments assert_called_with(@object, :<<, [ 2 ]) do @object << 2 end end + def test_assert_called_with_arguments_and_returns + assert_called_with(@object, :<<, [ 2 ], returns: 10) do + assert_equal(10, @object << 2) + end + + assert_nil(@object << 2) + end + def test_assert_called_with_failure assert_raises(MockExpectationError) do assert_called_with(@object, :<<, [ 4567 ]) do @@ -78,19 +82,72 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase end end - def test_assert_called_with_returns - assert_called_with(@object, :increment, returns: 1) do + def test_assert_called_with_multiple_expected_arguments + assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do + @object << 1 + @object << 2 + end + end + + def test_assert_called_on_instance_of_with_defaults_to_expect_once + assert_called_on_instance_of Level, :increment do @object.increment end end - def test_assert_called_with_multiple_expected_arguments - assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do - @object << 1 + def test_assert_called_on_instance_of_more_than_once + assert_called_on_instance_of(Level, :increment, times: 2) do + @object.increment + @object.increment + end + end + + def test_assert_called_on_instance_of_with_arguments + assert_called_on_instance_of(Level, :<<) do @object << 2 end end + def test_assert_called_on_instance_of_returns + assert_called_on_instance_of(Level, :increment, returns: 10) do + assert_equal 10, @object.increment + end + + assert_equal 1, @object.increment + end + + def test_assert_called_on_instance_of_failure + error = assert_raises(Minitest::Assertion) do + assert_called_on_instance_of(Level, :increment) do + # Call nothing... + end + end + + assert_equal "Expected increment to be called 1 times, but was called 0 times.\nExpected: 1\n Actual: 0", error.message + end + + def test_assert_called_on_instance_of_with_message + error = assert_raises(Minitest::Assertion) do + assert_called_on_instance_of(Level, :increment, "dang it") do + # Call nothing... + end + end + + assert_match(/dang it.\nExpected increment/, error.message) + end + + def test_assert_called_on_instance_of_nesting + assert_called_on_instance_of(Level, :increment, times: 3) do + assert_called_on_instance_of(Level, :decrement, times: 2) do + @object.increment + @object.decrement + @object.increment + @object.decrement + @object.increment + end + end + end + def test_assert_not_called assert_not_called(@object, :decrement) do @object.increment @@ -107,6 +164,30 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message end + def test_assert_not_called_on_instance_of + assert_not_called_on_instance_of(Level, :decrement) do + @object.increment + end + end + + def test_assert_not_called_on_instance_of_failure + error = assert_raises(Minitest::Assertion) do + assert_not_called_on_instance_of(Level, :increment) do + @object.increment + end + end + + assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message + end + + def test_assert_not_called_on_instance_of_nesting + assert_not_called_on_instance_of(Level, :increment) do + assert_not_called_on_instance_of(Level, :decrement) do + # Call nothing... + end + end + end + def test_stub_any_instance stub_any_instance(Level) do |instance| assert_equal instance, Level.new diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 9c2c635f43..8c47f2cdc7 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -186,4 +186,8 @@ class TimeTravelTest < ActiveSupport::TestCase assert_operator expected_time.to_s(:db), :<, Time.now.to_s(:db) end + + def test_time_helper_unfreeze_time + assert_equal method(:travel_back), method(:unfreeze_time) + end end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index 34bf81fa75..b711619ba7 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -12,7 +12,7 @@ class REXMLEngineTest < XMLMiniEngineTest end def test_parse_from_frozen_string - xml_string = "<root></root>".freeze + xml_string = "<root></root>" assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) end diff --git a/activesupport/test/xml_mini/xml_mini_engine_test.rb b/activesupport/test/xml_mini/xml_mini_engine_test.rb index 5c4c28d9b7..c62e7e32c9 100644 --- a/activesupport/test/xml_mini/xml_mini_engine_test.rb +++ b/activesupport/test/xml_mini/xml_mini_engine_test.rb @@ -78,7 +78,7 @@ class XMLMiniEngineTest < ActiveSupport::TestCase end def test_parse_from_frozen_string - xml_string = "<root/>".freeze + xml_string = "<root/>" assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) end diff --git a/ci/qunit-selenium-runner.rb b/ci/qunit-selenium-runner.rb index 05bcab8cdb..132b3d17eb 100644 --- a/ci/qunit-selenium-runner.rb +++ b/ci/qunit-selenium-runner.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "qunit/selenium/test_runner" -require "chromedriver/helper" +require "chromedriver-helper" driver_options = Selenium::WebDriver::Chrome::Options.new driver_options.add_argument("--headless") diff --git a/ci/travis.rb b/ci/travis.rb index 861063afa5..1de281046f 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -9,10 +9,10 @@ commands = [ 'mysql -e "grant all privileges on activerecord_unittest.* to rails@localhost;"', 'mysql -e "grant all privileges on activerecord_unittest2.* to rails@localhost;"', 'mysql -e "grant all privileges on inexistent_activerecord_unittest.* to rails@localhost;"', - 'mysql -e "create database activerecord_unittest;"', - 'mysql -e "create database activerecord_unittest2;"', - 'psql -c "create database activerecord_unittest;" -U postgres', - 'psql -c "create database activerecord_unittest2;" -U postgres' + 'mysql -e "create database activerecord_unittest default character set utf8mb4;"', + 'mysql -e "create database activerecord_unittest2 default character set utf8mb4;"', + 'psql -c "create database -E UTF8 -T template0 activerecord_unittest;" -U postgres', + 'psql -c "create database -E UTF8 -T template0 activerecord_unittest2;" -U postgres' ] commands.each do |command| diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 0307e06fd9..516b643cb8 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,7 @@ +* New section _Troubleshooting_ in the _Autoloading and Reloading Constants_ guide. + + *Xavier Noria* + * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/guides/assets/images/rails_guides_logo_1x.png b/guides/assets/images/rails_guides_logo_1x.png Binary files differnew file mode 100644 index 0000000000..8c6810c312 --- /dev/null +++ b/guides/assets/images/rails_guides_logo_1x.png diff --git a/guides/assets/images/rails_guides_logo_2x.png b/guides/assets/images/rails_guides_logo_2x.png Binary files differnew file mode 100644 index 0000000000..accc6bbfa4 --- /dev/null +++ b/guides/assets/images/rails_guides_logo_2x.png diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js index e39ac239cd..a37f5d1927 100644 --- a/guides/assets/javascripts/guides.js +++ b/guides/assets/javascripts/guides.js @@ -19,7 +19,18 @@ return elem; } - document.addEventListener("DOMContentLoaded", function() { + // For old browsers + this.each = function(node, callback) { + var array = Array.prototype.slice.call(node); + for(var i = 0; i < array.length; i++) callback(array[i]); + } + + // Viewable on local + if (window.location.protocol === "file:") Turbolinks.supported = false; + + document.addEventListener("turbolinks:load", function() { + window.SyntaxHighlighter.highlight({ "auto-links": false }); + var guidesMenu = document.getElementById("guidesMenu"); var guides = document.getElementById("guides"); @@ -28,12 +39,22 @@ guides.classList.toggle("visible"); }); + each(document.querySelectorAll("#guides a"), function(element) { + element.addEventListener("click", function(e) { + guides.classList.toggle("visible"); + }); + }); + var guidesIndexItem = document.querySelector("select.guides-index-item"); var currentGuidePath = window.location.pathname; guidesIndexItem.value = currentGuidePath.substring(currentGuidePath.lastIndexOf("/") + 1); guidesIndexItem.addEventListener("change", function(e) { - window.location = e.target.value; + if (Turbolinks.supported) { + Turbolinks.visit(e.target.value); + } else { + window.location = e.target.value; + } }); var moreInfoButton = document.querySelector(".more-info-button"); diff --git a/guides/assets/javascripts/responsive-tables.js b/guides/assets/javascripts/responsive-tables.js index 24906dddeb..1c0f28c993 100644 --- a/guides/assets/javascripts/responsive-tables.js +++ b/guides/assets/javascripts/responsive-tables.js @@ -3,16 +3,6 @@ var switched = false; - // For old browsers - var each = function(node, callback) { - var array = Array.prototype.slice.call(node); - for(var i = 0; i < array.length; i++) callback(array[i]); - } - - each(document.querySelectorAll(":not(.syntaxhighlighter)>table"), function(element) { - element.classList.add("responsive"); - }); - var updateTables = function() { if (document.documentElement.clientWidth < 767 && !switched) { switched = true; @@ -23,7 +13,13 @@ } } - document.addEventListener("DOMContentLoaded", updateTables); + document.addEventListener("turbolinks:load", function() { + each(document.querySelectorAll(":not(.syntaxhighlighter)>table"), function(element) { + element.classList.add("responsive"); + }); + updateTables(); + }); + window.addEventListener("resize", updateTables); var splitTable = function(original) { diff --git a/guides/assets/javascripts/turbolinks.js b/guides/assets/javascripts/turbolinks.js new file mode 100644 index 0000000000..686283c7f0 --- /dev/null +++ b/guides/assets/javascripts/turbolinks.js @@ -0,0 +1,6 @@ +/* +Turbolinks 5.1.1 +Copyright © 2018 Basecamp, LLC + */ +(function(){var t=this;(function(){(function(){this.Turbolinks={supported:function(){return null!=window.history.pushState&&null!=window.requestAnimationFrame&&null!=window.addEventListener}(),visit:function(t,r){return e.controller.visit(t,r)},clearCache:function(){return e.controller.clearCache()},setProgressBarDelay:function(t){return e.controller.setProgressBarDelay(t)}}}).call(this)}).call(t);var e=t.Turbolinks;(function(){(function(){var t,r,n,o=[].slice;e.copyObject=function(t){var e,r,n;r={};for(e in t)n=t[e],r[e]=n;return r},e.closest=function(e,r){return t.call(e,r)},t=function(){var t,e;return t=document.documentElement,null!=(e=t.closest)?e:function(t){var e;for(e=this;e;){if(e.nodeType===Node.ELEMENT_NODE&&r.call(e,t))return e;e=e.parentNode}}}(),e.defer=function(t){return setTimeout(t,1)},e.throttle=function(t){var e;return e=null,function(){var r;return r=1<=arguments.length?o.call(arguments,0):[],null!=e?e:e=requestAnimationFrame(function(n){return function(){return e=null,t.apply(n,r)}}(this))}},e.dispatch=function(t,e){var r,o,i,s,a,u;return a=null!=e?e:{},u=a.target,r=a.cancelable,o=a.data,i=document.createEvent("Events"),i.initEvent(t,!0,r===!0),i.data=null!=o?o:{},i.cancelable&&!n&&(s=i.preventDefault,i.preventDefault=function(){return this.defaultPrevented||Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}}),s.call(this)}),(null!=u?u:document).dispatchEvent(i),i},n=function(){var t;return t=document.createEvent("Events"),t.initEvent("test",!0,!0),t.preventDefault(),t.defaultPrevented}(),e.match=function(t,e){return r.call(t,e)},r=function(){var t,e,r,n;return t=document.documentElement,null!=(e=null!=(r=null!=(n=t.matchesSelector)?n:t.webkitMatchesSelector)?r:t.msMatchesSelector)?e:t.mozMatchesSelector}(),e.uuid=function(){var t,e,r;for(r="",t=e=1;36>=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){e.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.requestURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=t(this.requestCanceled,this),this.requestTimedOut=t(this.requestTimedOut,this),this.requestFailed=t(this.requestFailed,this),this.requestLoaded=t(this.requestLoaded,this),this.requestProgressed=t(this.requestProgressed,this),this.url=e.Location.wrap(n).requestURL,this.referrer=e.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.NETWORK_FAILURE)}}(this))},r.prototype.requestTimedOut=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.TIMEOUT_FAILURE)}}(this))},r.prototype.requestCanceled=function(){return this.endRequest()},r.prototype.notifyApplicationBeforeRequestStart=function(){return e.dispatch("turbolinks:request-start",{data:{url:this.url,xhr:this.xhr}})},r.prototype.notifyApplicationAfterRequestEnd=function(){return e.dispatch("turbolinks:request-end",{data:{url:this.url,xhr:this.xhr}})},r.prototype.createXHR=function(){return this.xhr=new XMLHttpRequest,this.xhr.open("GET",this.url,!0),this.xhr.timeout=1e3*this.constructor.timeout,this.xhr.setRequestHeader("Accept","text/html, application/xhtml+xml"),this.xhr.setRequestHeader("Turbolinks-Referrer",this.referrer),this.xhr.onprogress=this.requestProgressed,this.xhr.onload=this.requestLoaded,this.xhr.onerror=this.requestFailed,this.xhr.ontimeout=this.requestTimedOut,this.xhr.onabort=this.requestCanceled},r.prototype.endRequest=function(t){return this.xhr?(this.notifyApplicationAfterRequestEnd(),null!=t&&t.call(this),this.destroy()):void 0},r.prototype.setProgress=function(t){var e;return this.progress=t,"function"==typeof(e=this.delegate).requestProgressed?e.requestProgressed(this.progress):void 0},r.prototype.destroy=function(){var t;return this.setProgress(1),"function"==typeof(t=this.delegate).requestFinished&&t.requestFinished(),this.delegate=null,this.xhr=null},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.ProgressBar=function(){function e(){this.trickle=t(this.trickle,this),this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement()}var r;return r=300,e.defaultCSS=".turbolinks-progress-bar {\n position: fixed;\n display: block;\n top: 0;\n left: 0;\n height: 3px;\n background: #0076ff;\n z-index: 9999;\n transition: width "+r+"ms ease-out, opacity "+r/2+"ms "+r/2+"ms ease-in;\n transform: translate3d(0, 0, 0);\n}",e.prototype.show=function(){return this.visible?void 0:(this.visible=!0,this.installStylesheetElement(),this.installProgressElement(),this.startTrickling())},e.prototype.hide=function(){return this.visible&&!this.hiding?(this.hiding=!0,this.fadeProgressElement(function(t){return function(){return t.uninstallProgressElement(),t.stopTrickling(),t.visible=!1,t.hiding=!1}}(this))):void 0},e.prototype.setValue=function(t){return this.value=t,this.refresh()},e.prototype.installStylesheetElement=function(){return document.head.insertBefore(this.stylesheetElement,document.head.firstChild)},e.prototype.installProgressElement=function(){return this.progressElement.style.width=0,this.progressElement.style.opacity=1,document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()},e.prototype.fadeProgressElement=function(t){return this.progressElement.style.opacity=0,setTimeout(t,1.5*r)},e.prototype.uninstallProgressElement=function(){return this.progressElement.parentNode?document.documentElement.removeChild(this.progressElement):void 0},e.prototype.startTrickling=function(){return null!=this.trickleInterval?this.trickleInterval:this.trickleInterval=setInterval(this.trickle,r)},e.prototype.stopTrickling=function(){return clearInterval(this.trickleInterval),this.trickleInterval=null},e.prototype.trickle=function(){return this.setValue(this.value+Math.random()/100)},e.prototype.refresh=function(){return requestAnimationFrame(function(t){return function(){return t.progressElement.style.width=10+90*t.value+"%"}}(this))},e.prototype.createStylesheetElement=function(){var t;return t=document.createElement("style"),t.type="text/css",t.textContent=this.constructor.defaultCSS,t},e.prototype.createProgressElement=function(){var t;return t=document.createElement("div"),t.className="turbolinks-progress-bar",t},e}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.BrowserAdapter=function(){function r(r){this.controller=r,this.showProgressBar=t(this.showProgressBar,this),this.progressBar=new e.ProgressBar}var n,o,i;return i=e.HttpRequest,n=i.NETWORK_FAILURE,o=i.TIMEOUT_FAILURE,r.prototype.visitProposedToLocationWithAction=function(t,e){return this.controller.startVisitToLocationWithAction(t,e)},r.prototype.visitStarted=function(t){return t.issueRequest(),t.changeHistory(),t.loadCachedSnapshot()},r.prototype.visitRequestStarted=function(t){return this.progressBar.setValue(0),t.hasCachedSnapshot()||"restore"!==t.action?this.showProgressBarAfterDelay():this.showProgressBar()},r.prototype.visitRequestProgressed=function(t){return this.progressBar.setValue(t.progress)},r.prototype.visitRequestCompleted=function(t){return t.loadResponse()},r.prototype.visitRequestFailedWithStatusCode=function(t,e){switch(e){case n:case o:return this.reload();default:return t.loadResponse()}},r.prototype.visitRequestFinished=function(t){return this.hideProgressBar()},r.prototype.visitCompleted=function(t){return t.followRedirect()},r.prototype.pageInvalidated=function(){return this.reload()},r.prototype.showProgressBarAfterDelay=function(){return this.progressBarTimeout=setTimeout(this.showProgressBar,this.controller.progressBarDelay)},r.prototype.showProgressBar=function(){return this.progressBar.show()},r.prototype.hideProgressBar=function(){return this.progressBar.hide(),clearTimeout(this.progressBarTimeout)},r.prototype.reload=function(){return window.location.reload()},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.History=function(){function r(e){this.delegate=e,this.onPageLoad=t(this.onPageLoad,this),this.onPopState=t(this.onPopState,this)}return r.prototype.start=function(){return this.started?void 0:(addEventListener("popstate",this.onPopState,!1),addEventListener("load",this.onPageLoad,!1),this.started=!0)},r.prototype.stop=function(){return this.started?(removeEventListener("popstate",this.onPopState,!1),removeEventListener("load",this.onPageLoad,!1),this.started=!1):void 0},r.prototype.push=function(t,r){return t=e.Location.wrap(t),this.update("push",t,r)},r.prototype.replace=function(t,r){return t=e.Location.wrap(t),this.update("replace",t,r)},r.prototype.onPopState=function(t){var r,n,o,i;return this.shouldHandlePopState()&&(i=null!=(n=t.state)?n.turbolinks:void 0)?(r=e.Location.wrap(window.location),o=i.restorationIdentifier,this.delegate.historyPoppedToLocationWithRestorationIdentifier(r,o)):void 0},r.prototype.onPageLoad=function(t){return e.defer(function(t){return function(){return t.pageLoaded=!0}}(this))},r.prototype.shouldHandlePopState=function(){return this.pageIsLoaded()},r.prototype.pageIsLoaded=function(){return this.pageLoaded||"complete"===document.readyState},r.prototype.update=function(t,e,r){var n;return n={turbolinks:{restorationIdentifier:r}},history[t+"State"](n,null,e)},r}()}.call(this),function(){e.Snapshot=function(){function t(t){var e,r;r=t.head,e=t.body,this.head=null!=r?r:document.createElement("head"),this.body=null!=e?e:document.createElement("body")}return t.wrap=function(t){return t instanceof this?t:this.fromHTML(t)},t.fromHTML=function(t){var e;return e=document.createElement("html"),e.innerHTML=t,this.fromElement(e)},t.fromElement=function(t){return new this({head:t.querySelector("head"),body:t.querySelector("body")})},t.prototype.clone=function(){return new t({head:this.head.cloneNode(!0),body:this.body.cloneNode(!0)})},t.prototype.getRootLocation=function(){var t,r;return r=null!=(t=this.getSetting("root"))?t:"/",new e.Location(r)},t.prototype.getCacheControlValue=function(){return this.getSetting("cache-control")},t.prototype.getElementForAnchor=function(t){try{return this.body.querySelector("[id='"+t+"'], a[name='"+t+"']")}catch(e){}},t.prototype.hasAnchor=function(t){return null!=this.getElementForAnchor(t)},t.prototype.isPreviewable=function(){return"no-preview"!==this.getCacheControlValue()},t.prototype.isCacheable=function(){return"no-cache"!==this.getCacheControlValue()},t.prototype.isVisitable=function(){return"reload"!==this.getSetting("visit-control")},t.prototype.getSetting=function(t){var e,r;return r=this.head.querySelectorAll("meta[name='turbolinks-"+t+"']"),e=r[r.length-1],null!=e?e.getAttribute("content"):void 0},t}()}.call(this),function(){var t=[].slice;e.Renderer=function(){function e(){}var r;return e.render=function(){var e,r,n,o;return n=arguments[0],r=arguments[1],e=3<=arguments.length?t.call(arguments,2):[],o=function(t,e,r){r.prototype=t.prototype;var n=new r,o=t.apply(n,e);return Object(o)===o?o:n}(this,e,function(){}),o.delegate=n,o.render(r),o},e.prototype.renderView=function(t){return this.delegate.viewWillRender(this.newBody),t(),this.delegate.viewRendered(this.newBody)},e.prototype.invalidateView=function(){return this.delegate.viewInvalidated()},e.prototype.createScriptElement=function(t){var e;return"false"===t.getAttribute("data-turbolinks-eval")?t:(e=document.createElement("script"),e.textContent=t.textContent,e.async=!1,r(e,t),e)},r=function(t,e){var r,n,o,i,s,a,u;for(i=e.attributes,a=[],r=0,n=i.length;n>r;r++)s=i[r],o=s.name,u=s.value,a.push(t.setAttribute(o,u));return a},e}()}.call(this),function(){e.HeadDetails=function(){function t(t){var e,r,i,s,a,u,l;for(this.element=t,this.elements={},l=this.element.childNodes,s=0,u=l.length;u>s;s++)i=l[s],i.nodeType===Node.ELEMENT_NODE&&(a=i.outerHTML,r=null!=(e=this.elements)[a]?e[a]:e[a]={type:o(i),tracked:n(i),elements:[]},r.elements.push(i))}var e,r,n,o;return t.prototype.hasElementWithKey=function(t){return t in this.elements},t.prototype.getTrackedElementSignature=function(){var t,e;return function(){var r,n;r=this.elements,n=[];for(t in r)e=r[t].tracked,e&&n.push(t);return n}.call(this).join("")},t.prototype.getScriptElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("script",t)},t.prototype.getStylesheetElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("stylesheet",t)},t.prototype.getElementsMatchingTypeNotInDetails=function(t,e){var r,n,o,i,s,a;o=this.elements,s=[];for(n in o)i=o[n],a=i.type,r=i.elements,a!==t||e.hasElementWithKey(n)||s.push(r[0]);return s},t.prototype.getProvisionalElements=function(){var t,e,r,n,o,i,s;r=[],n=this.elements;for(e in n)o=n[e],s=o.type,i=o.tracked,t=o.elements,null!=s||i?t.length>1&&r.push.apply(r,t.slice(1)):r.push.apply(r,t);return r},o=function(t){return e(t)?"script":r(t)?"stylesheet":void 0},n=function(t){return"reload"===t.getAttribute("data-turbolinks-track")},e=function(t){var e;return e=t.tagName.toLowerCase(),"script"===e},r=function(t){var e;return e=t.tagName.toLowerCase(),"style"===e||"link"===e&&"stylesheet"===t.getAttribute("rel")},t}()}.call(this),function(){var t=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;e.SnapshotRenderer=function(r){function n(t,r,n){this.currentSnapshot=t,this.newSnapshot=r,this.isPreview=n,this.currentHeadDetails=new e.HeadDetails(this.currentSnapshot.head),this.newHeadDetails=new e.HeadDetails(this.newSnapshot.head),this.newBody=this.newSnapshot.body}return t(n,r),n.prototype.render=function(t){return this.shouldRender()?(this.mergeHead(),this.renderView(function(e){return function(){return e.replaceBody(),e.isPreview||e.focusFirstAutofocusableElement(),t()}}(this))):this.invalidateView()},n.prototype.mergeHead=function(){return this.copyNewHeadStylesheetElements(),this.copyNewHeadScriptElements(),this.removeCurrentHeadProvisionalElements(),this.copyNewHeadProvisionalElements()},n.prototype.replaceBody=function(){return this.activateBodyScriptElements(),this.importBodyPermanentElements(),this.assignNewBody()},n.prototype.shouldRender=function(){return this.newSnapshot.isVisitable()&&this.trackedElementsAreIdentical()},n.prototype.trackedElementsAreIdentical=function(){return this.currentHeadDetails.getTrackedElementSignature()===this.newHeadDetails.getTrackedElementSignature()},n.prototype.copyNewHeadStylesheetElements=function(){var t,e,r,n,o;for(n=this.getNewHeadStylesheetElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.copyNewHeadScriptElements=function(){var t,e,r,n,o;for(n=this.getNewHeadScriptElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(this.createScriptElement(t)));return o},n.prototype.removeCurrentHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getCurrentHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.removeChild(t));return o},n.prototype.copyNewHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getNewHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.importBodyPermanentElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyPermanentElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],(t=this.findCurrentBodyPermanentElement(o))?i.push(o.parentNode.replaceChild(t,o)):i.push(void 0);return i},n.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},n.prototype.assignNewBody=function(){return document.body=this.newBody},n.prototype.focusFirstAutofocusableElement=function(){var t;return null!=(t=this.findFirstAutofocusableElement())?t.focus():void 0},n.prototype.getNewHeadStylesheetElements=function(){return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails)},n.prototype.getNewHeadScriptElements=function(){return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails)},n.prototype.getCurrentHeadProvisionalElements=function(){return this.currentHeadDetails.getProvisionalElements()},n.prototype.getNewHeadProvisionalElements=function(){return this.newHeadDetails.getProvisionalElements()},n.prototype.getNewBodyPermanentElements=function(){return this.newBody.querySelectorAll("[id][data-turbolinks-permanent]")},n.prototype.findCurrentBodyPermanentElement=function(t){return document.body.querySelector("#"+t.id+"[data-turbolinks-permanent]")},n.prototype.getNewBodyScriptElements=function(){return this.newBody.querySelectorAll("script")},n.prototype.findFirstAutofocusableElement=function(){return document.body.querySelector("[autofocus]")},n}(e.Renderer)}.call(this),function(){var t=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;e.ErrorRenderer=function(e){function r(t){this.html=t}return t(r,e),r.prototype.render=function(t){return this.renderView(function(e){return function(){return e.replaceDocumentHTML(),e.activateBodyScriptElements(),t()}}(this))},r.prototype.replaceDocumentHTML=function(){return document.documentElement.innerHTML=this.html},r.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},r.prototype.getScriptElements=function(){return document.documentElement.querySelectorAll("script")},r}(e.Renderer)}.call(this),function(){e.View=function(){function t(t){this.delegate=t,this.element=document.documentElement}return t.prototype.getRootLocation=function(){return this.getSnapshot().getRootLocation()},t.prototype.getElementForAnchor=function(t){return this.getSnapshot().getElementForAnchor(t)},t.prototype.getSnapshot=function(){return e.Snapshot.fromElement(this.element)},t.prototype.render=function(t,e){var r,n,o;return o=t.snapshot,r=t.error,n=t.isPreview,this.markAsPreview(n),null!=o?this.renderSnapshot(o,n,e):this.renderError(r,e)},t.prototype.markAsPreview=function(t){return t?this.element.setAttribute("data-turbolinks-preview",""):this.element.removeAttribute("data-turbolinks-preview")},t.prototype.renderSnapshot=function(t,r,n){return e.SnapshotRenderer.render(this.delegate,n,this.getSnapshot(),e.Snapshot.wrap(t),r)},t.prototype.renderError=function(t,r){return e.ErrorRenderer.render(this.delegate,r,t)},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.ScrollManager=function(){function r(r){this.delegate=r,this.onScroll=t(this.onScroll,this),this.onScroll=e.throttle(this.onScroll)}return r.prototype.start=function(){return this.started?void 0:(addEventListener("scroll",this.onScroll,!1),this.onScroll(),this.started=!0)},r.prototype.stop=function(){return this.started?(removeEventListener("scroll",this.onScroll,!1),this.started=!1):void 0},r.prototype.scrollToElement=function(t){return t.scrollIntoView()},r.prototype.scrollToPosition=function(t){var e,r;return e=t.x,r=t.y,window.scrollTo(e,r)},r.prototype.onScroll=function(t){return this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})},r.prototype.updatePosition=function(t){var e;return this.position=t,null!=(e=this.delegate)?e.scrollPositionChanged(this.position):void 0},r}()}.call(this),function(){e.SnapshotCache=function(){function t(t){this.size=t,this.keys=[],this.snapshots={}}var r;return t.prototype.has=function(t){var e;return e=r(t),e in this.snapshots},t.prototype.get=function(t){var e;if(this.has(t))return e=this.read(t),this.touch(t),e},t.prototype.put=function(t,e){return this.write(t,e),this.touch(t),e},t.prototype.read=function(t){var e;return e=r(t),this.snapshots[e]},t.prototype.write=function(t,e){var n;return n=r(t),this.snapshots[n]=e},t.prototype.touch=function(t){var e,n;return n=r(t),e=this.keys.indexOf(n),e>-1&&this.keys.splice(e,1),this.keys.unshift(n),this.trim()},t.prototype.trim=function(){var t,e,r,n,o;for(n=this.keys.splice(this.size),o=[],t=0,r=n.length;r>t;t++)e=n[t],o.push(delete this.snapshots[e]);return o},r=function(t){return e.Location.wrap(t).toCacheKey()},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.Visit=function(){function r(r,n,o){this.controller=r,this.action=o,this.performScroll=t(this.performScroll,this),this.identifier=e.uuid(),this.location=e.Location.wrap(n),this.adapter=this.controller.adapter,this.state="initialized",this.timingMetrics={}}var n;return r.prototype.start=function(){return"initialized"===this.state?(this.recordTimingMetric("visitStart"),this.state="started",this.adapter.visitStarted(this)):void 0},r.prototype.cancel=function(){var t;return"started"===this.state?(null!=(t=this.request)&&t.cancel(),this.cancelRender(),this.state="canceled"):void 0},r.prototype.complete=function(){var t;return"started"===this.state?(this.recordTimingMetric("visitEnd"),this.state="completed","function"==typeof(t=this.adapter).visitCompleted&&t.visitCompleted(this),this.controller.visitCompleted(this)):void 0},r.prototype.fail=function(){var t;return"started"===this.state?(this.state="failed","function"==typeof(t=this.adapter).visitFailed?t.visitFailed(this):void 0):void 0},r.prototype.changeHistory=function(){var t,e;return this.historyChanged?void 0:(t=this.location.isEqualTo(this.referrer)?"replace":this.action,e=n(t),this.controller[e](this.location,this.restorationIdentifier),this.historyChanged=!0)},r.prototype.issueRequest=function(){return this.shouldIssueRequest()&&null==this.request?(this.progress=0,this.request=new e.HttpRequest(this,this.location,this.referrer),this.request.send()):void 0},r.prototype.getCachedSnapshot=function(){var t;return!(t=this.controller.getCachedSnapshotForLocation(this.location))||null!=this.location.anchor&&!t.hasAnchor(this.location.anchor)||"restore"!==this.action&&!t.isPreviewable()?void 0:t},r.prototype.hasCachedSnapshot=function(){return null!=this.getCachedSnapshot()},r.prototype.loadCachedSnapshot=function(){var t,e;return(e=this.getCachedSnapshot())?(t=this.shouldIssueRequest(),this.render(function(){var r;return this.cacheSnapshot(),this.controller.render({snapshot:e,isPreview:t},this.performScroll),"function"==typeof(r=this.adapter).visitRendered&&r.visitRendered(this),t?void 0:this.complete()})):void 0},r.prototype.loadResponse=function(){return null!=this.response?this.render(function(){var t,e;return this.cacheSnapshot(),this.request.failed?(this.controller.render({error:this.response},this.performScroll),"function"==typeof(t=this.adapter).visitRendered&&t.visitRendered(this),this.fail()):(this.controller.render({snapshot:this.response},this.performScroll),"function"==typeof(e=this.adapter).visitRendered&&e.visitRendered(this),this.complete())}):void 0},r.prototype.followRedirect=function(){return this.redirectedToLocation&&!this.followedRedirect?(this.location=this.redirectedToLocation,this.controller.replaceHistoryWithLocationAndRestorationIdentifier(this.redirectedToLocation,this.restorationIdentifier),this.followedRedirect=!0):void 0},r.prototype.requestStarted=function(){var t;return this.recordTimingMetric("requestStart"),"function"==typeof(t=this.adapter).visitRequestStarted?t.visitRequestStarted(this):void 0},r.prototype.requestProgressed=function(t){var e;return this.progress=t,"function"==typeof(e=this.adapter).visitRequestProgressed?e.visitRequestProgressed(this):void 0},r.prototype.requestCompletedWithResponse=function(t,r){return this.response=t,null!=r&&(this.redirectedToLocation=e.Location.wrap(r)),this.adapter.visitRequestCompleted(this)},r.prototype.requestFailedWithStatusCode=function(t,e){return this.response=e,this.adapter.visitRequestFailedWithStatusCode(this,t)},r.prototype.requestFinished=function(){var t;return this.recordTimingMetric("requestEnd"),"function"==typeof(t=this.adapter).visitRequestFinished?t.visitRequestFinished(this):void 0},r.prototype.performScroll=function(){return this.scrolled?void 0:("restore"===this.action?this.scrollToRestoredPosition()||this.scrollToTop():this.scrollToAnchor()||this.scrollToTop(),this.scrolled=!0)},r.prototype.scrollToRestoredPosition=function(){var t,e;return t=null!=(e=this.restorationData)?e.scrollPosition:void 0,null!=t?(this.controller.scrollToPosition(t),!0):void 0},r.prototype.scrollToAnchor=function(){return null!=this.location.anchor?(this.controller.scrollToAnchor(this.location.anchor),!0):void 0},r.prototype.scrollToTop=function(){return this.controller.scrollToPosition({x:0,y:0})},r.prototype.recordTimingMetric=function(t){var e;return null!=(e=this.timingMetrics)[t]?e[t]:e[t]=(new Date).getTime()},r.prototype.getTimingMetrics=function(){return e.copyObject(this.timingMetrics)},n=function(t){switch(t){case"replace":return"replaceHistoryWithLocationAndRestorationIdentifier";case"advance":case"restore":return"pushHistoryWithLocationAndRestorationIdentifier"}},r.prototype.shouldIssueRequest=function(){return"restore"===this.action?!this.hasCachedSnapshot():!0},r.prototype.cacheSnapshot=function(){return this.snapshotCached?void 0:(this.controller.cacheSnapshot(),this.snapshotCached=!0)},r.prototype.render=function(t){return this.cancelRender(),this.frame=requestAnimationFrame(function(e){return function(){return e.frame=null,t.call(e)}}(this))},r.prototype.cancelRender=function(){return this.frame?cancelAnimationFrame(this.frame):void 0},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.Controller=function(){function r(){this.clickBubbled=t(this.clickBubbled,this),this.clickCaptured=t(this.clickCaptured,this),this.pageLoaded=t(this.pageLoaded,this),this.history=new e.History(this),this.view=new e.View(this),this.scrollManager=new e.ScrollManager(this),this.restorationData={},this.clearCache(),this.setProgressBarDelay(500)}return r.prototype.start=function(){return e.supported&&!this.started?(addEventListener("click",this.clickCaptured,!0),addEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.start(),this.startHistory(),this.started=!0,this.enabled=!0):void 0},r.prototype.disable=function(){return this.enabled=!1},r.prototype.stop=function(){return this.started?(removeEventListener("click",this.clickCaptured,!0),removeEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.stop(),this.stopHistory(),this.started=!1):void 0},r.prototype.clearCache=function(){return this.cache=new e.SnapshotCache(10)},r.prototype.visit=function(t,r){var n,o;return null==r&&(r={}),t=e.Location.wrap(t),this.applicationAllowsVisitingLocation(t)?this.locationIsVisitable(t)?(n=null!=(o=r.action)?o:"advance",this.adapter.visitProposedToLocationWithAction(t,n)):window.location=t:void 0},r.prototype.startVisitToLocationWithAction=function(t,r,n){var o;return e.supported?(o=this.getRestorationDataForIdentifier(n),this.startVisit(t,r,{restorationData:o})):window.location=t},r.prototype.setProgressBarDelay=function(t){return this.progressBarDelay=t},r.prototype.startHistory=function(){return this.location=e.Location.wrap(window.location),this.restorationIdentifier=e.uuid(),this.history.start(),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.stopHistory=function(){return this.history.stop()},r.prototype.pushHistoryWithLocationAndRestorationIdentifier=function(t,r){return this.restorationIdentifier=r,this.location=e.Location.wrap(t),this.history.push(this.location,this.restorationIdentifier)},r.prototype.replaceHistoryWithLocationAndRestorationIdentifier=function(t,r){return this.restorationIdentifier=r,this.location=e.Location.wrap(t),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.historyPoppedToLocationWithRestorationIdentifier=function(t,r){var n;return this.restorationIdentifier=r,this.enabled?(n=this.getRestorationDataForIdentifier(this.restorationIdentifier),this.startVisit(t,"restore",{restorationIdentifier:this.restorationIdentifier,restorationData:n,historyChanged:!0}),this.location=e.Location.wrap(t)):this.adapter.pageInvalidated()},r.prototype.getCachedSnapshotForLocation=function(t){var e;return e=this.cache.get(t),e?e.clone():void 0},r.prototype.shouldCacheSnapshot=function(){return this.view.getSnapshot().isCacheable()},r.prototype.cacheSnapshot=function(){var t;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),t=this.view.getSnapshot(),this.cache.put(this.lastRenderedLocation,t.clone())):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=this.view.getElementForAnchor(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r), +this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(t,r){return e.dispatch("turbolinks:click",{target:t,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(t){return e.dispatch("turbolinks:before-visit",{data:{url:t.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(t){return e.dispatch("turbolinks:visit",{data:{url:t.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return e.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(t){return e.dispatch("turbolinks:before-render",{data:{newBody:t}})},r.prototype.notifyApplicationAfterRender=function(){return e.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(t){return null==t&&(t={}),e.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:t}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(t,r,n){var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new e.Visit(this,t,r),u.restorationIdentifier=null!=a?a:e.uuid(),u.restorationData=e.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(t){return this.nodeIsVisitable(t)?e.closest(t,"a[href]:not([target]):not([download])"):void 0},r.prototype.getVisitableLocationForLink=function(t){var r;return r=new e.Location(t.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(t){var r;return(r=e.closest(t,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){!function(){var t,e;if((t=e=document.currentScript)&&!e.hasAttribute("data-turbolinks-suppress-warning"))for(;t=t.parentNode;)if(t===document.body)return console.warn("You are loading Turbolinks from a <script> element inside the <body> element. This is probably not what you meant to do!\n\nLoad your application\u2019s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\nFor more information, see: https://github.com/turbolinks/turbolinks#working-with-script-elements\n\n\u2014\u2014\nSuppress this warning by adding a `data-turbolinks-suppress-warning` attribute to: %s",e.outerHTML)}()}.call(this),function(){var t,r,n;e.start=function(){return r()?(null==e.controller&&(e.controller=t()),e.controller.start()):void 0},r=function(){return null==window.Turbolinks&&(window.Turbolinks=e),n()},t=function(){var t;return t=new e.Controller,t.adapter=new e.BrowserAdapter(t),t},n=function(){return window.Turbolinks===e},n()&&e.start()}.call(this)}).call(this),"object"==typeof module&&module.exports?module.exports=e:"function"==typeof define&&define.amd&&define(e)}).call(this); diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index cd355b1d1a..bdc3e21977 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -283,8 +283,12 @@ body { #header .wrapper, #topNav .wrapper, #feature .wrapper {padding-left: 1em; max-width: 960px;} #feature .wrapper {max-width: 640px; padding-right: 23em; position: relative; z-index: 0;} +@media screen and (max-width: 960px) { + #container .wrapper { padding-right: 23em; } +} + @media screen and (max-width: 800px) { - #feature .wrapper { padding-right: 0; } + #feature .wrapper, #container .wrapper { padding-right: 0; } } /* Links @@ -397,14 +401,10 @@ a, a:link, a:visited { } #guides { - width: 27em; + width: 37em; display: block; background: #980905; border-radius: 1em; - -webkit-border-radius: 1em; - -moz-border-radius: 1em; - -webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25); - -moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em; color: #f1938c; padding: 1.5em 2em; position: absolute; @@ -418,17 +418,44 @@ a, a:link, a:visited { display: block !important; } -#guides dt, #guides dd { +.guides-section dt, .guides-section dd { font-weight: normal; font-size: 0.722em; margin: 0; padding: 0; } -#guides dt {padding:0; margin: 0.5em 0 0;} -#guides a {color: #FFF; background: none !important; text-decoration: none;} -#guides a:hover {text-decoration: underline;} -#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;} -#guides .R {float: right;} +.guides-section dt { + margin: 0.5em 0 0; + padding:0; +} +#guides a { + background: none !important; + color: #FFF; + text-decoration: none; +} +#guides a:hover { + text-decoration: underline; +} +.guides-section-container { + display: flex; + flex-direction: column; + flex-wrap: wrap; + width: 100%; + max-height: 35em; +} + +.guides-section { + min-width: 5em; + margin: 0 2em 0.5em 0; + flex: auto; + max-width: 12em; +} + +.guides-section dd { + line-height: 1.3; + margin-bottom: 0.5em; +} + #guides hr { display: block; border: none; @@ -511,13 +538,26 @@ h6 { #header h1 { float: left; - background: url(../images/rails_guides_logo.gif) no-repeat; + background: url(../images/rails_guides_logo_1x.png) no-repeat; width: 297px; text-indent: -9999em; margin: 0; padding: 0; } +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and ( min--moz-device-pixel-ratio: 2), +only screen and ( -o-min-device-pixel-ratio: 2/1), +only screen and ( min-device-pixel-ratio: 2), +only screen and ( min-resolution: 192dpi), +only screen and ( min-resolution: 2dppx) { + #header h1 { + background: url(../images/rails_guides_logo_2x.png) no-repeat; + background-size: 160%; + } +} + @media screen and (max-width: 480px) { #header h1 { float: none; diff --git a/guides/assets/stylesheets/style.css b/guides/assets/stylesheets/style.css index 89b2ab885a..3dad5124f4 100644 --- a/guides/assets/stylesheets/style.css +++ b/guides/assets/stylesheets/style.css @@ -11,3 +11,4 @@ Import advanced style sheet @import url("reset.css"); @import url("main.css"); +@import url("turbolinks.css"); diff --git a/guides/assets/stylesheets/turbolinks.css b/guides/assets/stylesheets/turbolinks.css new file mode 100644 index 0000000000..5cb598cef2 --- /dev/null +++ b/guides/assets/stylesheets/turbolinks.css @@ -0,0 +1,3 @@ +.turbolinks-progress-bar { + background-color: #c52f24; +} diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index e8b6ad19dd..6c74200761 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -42,9 +37,6 @@ end require "minitest/autorun" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - class BugTest < Minitest::Test include Rack::Test::Methods diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index ffd81c0079..6d53e957d9 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index 720b7e9c51..eb9d1316e9 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -19,9 +14,6 @@ end require "minitest/autorun" require "active_job" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - class BuggyJob < ActiveJob::Base def perform puts "performed" diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb index 4bcee07607..ae3ef7752f 100644 --- a/guides/bug_report_templates/active_job_master.rb +++ b/guides/bug_report_templates/active_job_master.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -18,9 +13,6 @@ end require "active_job" require "minitest/autorun" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - class BuggyJob < ActiveJob::Base def perform puts "performed" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index c0d705239b..d88304a219 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -21,9 +16,6 @@ require "active_record" require "minitest/autorun" require "logger" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - # This connection will do for database-independent bug reports. ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 914f04f51a..780456b7b6 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index f47cf08766..5dfd49fb38 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -21,9 +16,6 @@ require "active_record" require "minitest/autorun" require "logger" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - # This connection will do for database-independent bug reports. ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb index 715dca98ba..b0fe3bc660 100644 --- a/guides/bug_report_templates/active_record_migrations_master.rb +++ b/guides/bug_report_templates/active_record_migrations_master.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -20,9 +15,6 @@ require "active_record" require "minitest/autorun" require "logger" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - # This connection will do for database-independent bug reports. ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb index 046572148b..4a8ce787c7 100644 --- a/guides/bug_report_templates/benchmark.rb +++ b/guides/bug_report_templates/benchmark.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index 0935354bf4..3fd54437f7 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" @@ -20,9 +15,6 @@ require "active_support" require "active_support/core_ext/object/blank" require "minitest/autorun" -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - class BugTest < Minitest::Test def test_stuff assert "zomg".present? diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index 727f428960..ec65fee292 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true -begin - require "bundler/inline" -rescue LoadError => e - $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" - raise e -end +require "bundler/inline" gemfile(true) do source "https://rubygems.org" diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index c48af797fa..2213ef754d 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -12,8 +12,8 @@ module RailsGuides n = s.length m = t.length - return m if (0 == n) - return n if (0 == m) + return m if 0 == n + return n if 0 == m d = (0..m).to_a x = nil diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 61b371363e..a98aa8fe66 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -103,7 +103,7 @@ module RailsGuides hierarchy = hierarchy[0, 3] + [node] end - node[:id] = dom_id(hierarchy) + node[:id] = dom_id(hierarchy) unless node[:id] node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}" end end diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 8095b8c898..f186ac526f 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -29,7 +29,12 @@ HTML # Always increase the heading level by 1, so we can use h1, h2 heading in the document header_level += 1 - %(<h#{header_level}>#{text}</h#{header_level}>) + header_with_id = text.scan(/(.*){#(.*)}/) + unless header_with_id.empty? + %(<h#{header_level} id="#{header_with_id[0][1].strip}">#{header_with_id[0][0].strip}</h#{header_level}>) + else + %(<h#{header_level}>#{text}</h#{header_level}>) + end end def paragraph(text) diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index f85415ee42..ee9a499953 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -52,7 +52,7 @@ After some versions without an upgrade, Rails 2.3 offers some new features for R Documentation ------------- -The [Ruby on Rails guides](https://guides.rubyonrails.org/) project has published several additional guides for Rails 2.3. In addition, a [separate site](http://edgeguides.rubyonrails.org/) maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the [Rails wiki](http://newwiki.rubyonrails.org/) and early planning for a Rails Book. +The [Ruby on Rails guides](https://guides.rubyonrails.org/) project has published several additional guides for Rails 2.3. In addition, a [separate site](https://edgeguides.rubyonrails.org/) maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the [Rails wiki](http://newwiki.rubyonrails.org/) and early planning for a Rails Book. * More Information: [Rails Documentation Projects](https://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects) diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 9d15dfb2aa..e936644daf 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -38,7 +38,7 @@ If you're upgrading an existing application, it's a great idea to have good test Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2. -TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing. +TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing. ### Rails Application object @@ -153,7 +153,7 @@ More information: - [New Action Mailer API in Rails 3](http://lindsaar.net/2010/ Documentation ------------- -The documentation in the Rails tree is being updated with all the API changes, additionally, the [Rails Edge Guides](http://edgeguides.rubyonrails.org/) are being updated one by one to reflect the changes in Rails 3.0. The guides at [guides.rubyonrails.org](https://guides.rubyonrails.org/) however will continue to contain only the stable version of Rails (at this point, version 2.3.5, until 3.0 is released). +The documentation in the Rails tree is being updated with all the API changes, additionally, the [Rails Edge Guides](https://edgeguides.rubyonrails.org/) are being updated one by one to reflect the changes in Rails 3.0. The guides at [guides.rubyonrails.org](https://guides.rubyonrails.org/) however will continue to contain only the stable version of Rails (at this point, version 2.3.5, until 3.0 is released). More Information: - [Rails Documentation Projects](https://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects) diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 8c3dc3454d..d6981656ee 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -26,7 +26,7 @@ If you're upgrading an existing application, it's a great idea to have good test Rails 3.1 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.1 is also compatible with Ruby 1.9.2. -TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing. +TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing. ### What to update in your apps diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index eaae695dff..c9bc7f937b 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -70,7 +70,7 @@ Major Features ### ActionPack -* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). +* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow permitted parameters to update model objects (`params.permit(:title, :text)`). * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. @@ -196,7 +196,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a ### Deprecations -* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead. +* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from minitest instead. * `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 0c7bd01cac..b236f7ca24 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -719,7 +719,7 @@ for detailed changes. responsibilities within a class. ([Commit](https://github.com/rails/rails/commit/1eee0ca6de975b42524105a59e0521d18b38ab81)) -* Added `Object#presence_in` to simplify value whitelisting. +* Added `Object#presence_in` to simplify adding values to a permitted list. ([Commit](https://github.com/rails/rails/commit/4edca106daacc5a159289eae255207d160f22396)) diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index f7c40d19e9..51d06bd07d 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -44,7 +44,7 @@ to their respective adapters. Active Job comes pre-configured with an inline runner that executes jobs right away. Jobs often need to take Active Record objects as arguments. Active Job passes -object references as URIs (uniform resource identifiers) instead of marshaling +object references as URIs (uniform resource identifiers) instead of marshalling the object itself. The new [Global ID](https://github.com/rails/globalid) library builds URIs and looks up the objects they reference. Passing Active Record objects as job arguments just works by using Global ID internally. diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md index e57ef03518..d63921507d 100644 --- a/guides/source/5_0_release_notes.md +++ b/guides/source/5_0_release_notes.md @@ -169,7 +169,7 @@ It includes some of these notable advancements: instead of waiting for the suite to complete. - Defer test output until the end of a full test run using the `-d` option. - Complete exception backtrace output using `-b` option. -- Integration with `Minitest` to allow options like `-s` for test seed data, +- Integration with minitest to allow options like `-s` for test seed data, `-n` for running specific test by name, `-v` for better verbose output and so forth. - Colored test output. diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md index d26d3d3b95..a5a7eb4b2e 100644 --- a/guides/source/5_1_release_notes.md +++ b/guides/source/5_1_release_notes.md @@ -399,7 +399,7 @@ Please refer to the [Changelog][action-view] for detailed changes. * Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` fields. - ([Pull Request](https://github.com/rails/rails/pull/28061)) + ([Pull Request](https://github.com/rails/rails/pull/25469)) * New Builder-style syntax for HTML tags (`tag.div`, `tag.br`, etc.) ([Pull Request](https://github.com/rails/rails/pull/25543)) diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md new file mode 100644 index 0000000000..f3ed21dc45 --- /dev/null +++ b/guides/source/6_0_release_notes.md @@ -0,0 +1,175 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.** + +Ruby on Rails 6.0 Release Notes +=============================== + +Highlights in Rails 6.0: + +* Parallel Testing + +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/6-0-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 6.0 +---------------------- + +If you're upgrading an existing application, it's a great idea to have good test +coverage before going in. You should also first upgrade to Rails 5.2 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 6.0. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-2-to-rails-6-0) +guide. + +Major Features +-------------- + +### Parallel Testing + +[Pull Request](https://github.com/rails/rails/pull/31900) + +[Parallel Testing](testing.html#parallel-testing) allows you to parallelize your +test suite. While forking processes is the default method, threading is +supported as well. Running tests in parallel reduces the time it takes +your entire test suite to run. + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action Cable +------------ + +Please refer to the [Changelog][action-cable] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action View +----------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Storage +-------------- + +Please refer to the [Changelog][active-storage] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Active Job +---------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Removals + +### Deprecations + +### Notable changes + +Ruby on Rails Guides +-------------------- + +Please refer to the [Changelog][guides] for detailed changes. + +### Notable changes + +Credits +------- + +See the +[full list of contributors to Rails](http://contributors.rubyonrails.org/) +for the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/6-0-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/6-0-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/6-0-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/6-0-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/6-0-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/6-0-stable/activerecord/CHANGELOG.md +[active-storage]: https://github.com/rails/rails/blob/6-0-stable/activestorage/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/6-0-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/6-0-stable/activejob/CHANGELOG.md +[guides]: https://github.com/rails/rails/blob/6-0-stable/guides/CHANGELOG.md diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 14c859994c..e6c0ae31a8 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -665,7 +665,7 @@ The above will start a cable server on port 28080. The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle -authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). +authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/). ## Dependencies diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 7ce1f5c2a3..aa746e4731 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -166,7 +166,7 @@ NOTE: Support for parsing XML parameters has been extracted into a gem named `ac The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: ```ruby -get '/clients/:status' => 'clients#index', foo: 'bar' +get '/clients/:status', to: 'clients#index', foo: 'bar' ``` In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients". @@ -193,8 +193,8 @@ In a given request, the method is not actually called for every single generated With strong parameters, Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been -whitelisted. This means that you'll have to make a conscious decision about -which attributes to allow for mass update. This is a better security +permitted. This means that you'll have to make a conscious decision about +which attributes to permit for mass update. This is a better security practice to help prevent accidentally allowing users to update sensitive model attributes. @@ -241,7 +241,7 @@ Given params.permit(:id) ``` -the key `:id` will pass the whitelisting if it appears in `params` and +the key `:id` will be permitted for inclusion if it appears in `params` and it has a permitted scalar value associated. Otherwise, the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected. @@ -269,7 +269,7 @@ but be careful because this opens the door to arbitrary input. In this case, `permit` ensures values in the returned structure are permitted scalars and filters out anything else. -To whitelist an entire hash of parameters, the `permit!` method can be +To permit an entire hash of parameters, the `permit!` method can be used: ```ruby @@ -291,7 +291,7 @@ params.permit(:name, { emails: [] }, { family: [ :name ], hobbies: [] }]) ``` -This declaration whitelists the `name`, `emails`, and `friends` +This declaration permits the `name`, `emails`, and `friends` attributes. It is expected that `emails` will be an array of permitted scalar values, and that `friends` will be an array of resources with specific attributes: they should have a `name` attribute (any @@ -326,7 +326,7 @@ parameters when you use `accepts_nested_attributes_for` in combination with a `has_many` association: ```ruby -# To whitelist the following data: +# To permit the following data: # {"book" => {"title" => "Some Book", # "chapters_attributes" => { "1" => {"title" => "First Chapter"}, # "2" => {"title" => "Second Chapter"}}}} @@ -336,7 +336,7 @@ params.require(:book).permit(:title, chapters_attributes: [:title]) Imagine a scenario where you have parameters representing a product name and a hash of arbitrary data associated with that product, and -you want to whitelist the product name attribute and also the whole +you want to permit the product name attribute and also the whole data hash: ```ruby @@ -349,7 +349,7 @@ end The strong parameter API was designed with the most common use cases in mind. It is not meant as a silver bullet to handle all of your -whitelisting problems. However, you can easily mix the API with your +parameter filtering problems. However, you can easily mix the API with your own code to adapt to your situation. Session diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 6c5f03ab38..1acb993cad 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -422,6 +422,21 @@ use the rendered text for the text part. The render command is the same one used inside of Action Controller, so you can use all the same options, such as `:text`, `:inline` etc. +If you would like to render a template located outside of the default `app/views/mailer_name/` directory, you can apply the `prepend_view_path`, like so: + +```ruby +class UserMailer < ApplicationMailer + prepend_view_path "custom/path/to/mailer/view" + + # This will try to load "custom/path/to/mailer/view/welcome_email" template + def welcome_email + # ... + end +end +``` + +You can also consider using the [append_view_path](https://guides.rubyonrails.org/action_view_overview.html#view-paths) method. + #### Caching mailer view You can perform fragment caching in mailer views like in application views using the `cache` method. @@ -772,7 +787,7 @@ files (environment.rb, production.rb, etc...) |`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i`.</li></ul>| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| |`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| -|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.| +|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing. If this value is `false`, `deliveries` array will not be populated even if `delivery_method` is `:test`.| |`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.| |`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).| @@ -824,13 +839,14 @@ Mailer Testing You can find detailed instructions on how to test your mailers in the [testing guide](testing.html#testing-your-mailers). -Intercepting Emails +Intercepting and Observing Emails ------------------- -There are situations where you need to edit an email before it's -delivered. Fortunately Action Mailer provides hooks to intercept every -email. You can register an interceptor to make modifications to mail messages -right before they are handed to the delivery agents. +Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to register classes that are called during the mail delivery life cycle of every email sent. + +### Intercepting Emails + +Interceptors allow you to make modifications to emails before they are handed off to the delivery agents. An interceptor class must implement the `:delivering_email(message)` method which will be called before the email is sent. ```ruby class SandboxEmailInterceptor @@ -854,3 +870,21 @@ NOTE: The example above uses a custom environment called "staging" for a production like server but for testing purposes. You can read [Creating Rails environments](configuring.html#creating-rails-environments) for more information about custom Rails environments. + +### Observing Emails + +Observers give you access to the email message after it has been sent. An observer class must implement the `:delivered_email(message)` method, which will be called after the email is sent. + +```ruby +class EmailDeliveryObserver + def self.delivered_email(message) + EmailDelivery.log(message) + end +end +``` +Like interceptors, you need to register observers with the Action Mailer framework. You can do this in an initializer file +`config/initializers/email_delivery_observer.rb` + +```ruby +ActionMailer::Base.register_observer(EmailDeliveryObserver) +``` diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index d90ea2e26a..b9e24099b1 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -82,9 +82,9 @@ of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: -* Database Table - Plural with underscores separating words (e.g., `book_clubs`). * Model Class - Singular with the first letter of each word capitalized (e.g., `BookClub`). +* Database Table - Plural with underscores separating words (e.g., `book_clubs`). | Model / Class | Table / Schema | | ---------------- | -------------- | @@ -115,7 +115,7 @@ to Active Record instances: * `created_at` - Automatically gets set to the current date and time when the record is first created. * `updated_at` - Automatically gets set to the current date and time whenever - the record is updated. + the record is created or updated. * `lock_version` - Adds [optimistic locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to a model. diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 5b06ff78bb..ebdee446f9 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -319,6 +319,14 @@ class Order < ApplicationRecord end ``` +As the proc is evaluated in the context of the object, it is also possible to write this as: + +```ruby +class Order < ApplicationRecord + before_save :normalize_card_number, if: Proc.new { paid_with_card? } +end +``` + ### Multiple Conditions for Callbacks When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration: @@ -427,7 +435,9 @@ class PictureFile < ApplicationRecord end ``` -WARNING. The `after_commit` and `after_rollback` callbacks are called for all models created, updated, or destroyed within a transaction block. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. As such, if your callback code could raise an exception, you'll need to rescue it and handle it within the callback in order to allow other callbacks to run. +WARNING. When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that transaction. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. As such, if your callback code could raise an exception, you'll need to rescue it and handle it within the callback in order to allow other callbacks to run. + +WARNING. The code executed within `after_commit` or `after_rollback` callbacks is itself not enclosed within a transaction. WARNING. Using both `after_create_commit` and `after_update_commit` in the same model will only allow the last callback defined to take effect, and will override all others. diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index cfa444fda0..4d195988f8 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -491,6 +491,9 @@ NOTE: Active Record only supports single column foreign keys. `execute` and `structure.sql` are required to use composite foreign keys. See [Schema Dumping and You](#schema-dumping-and-you). +NOTE: The SQLite3 adapter doesn't support `add_foreign_key` since SQLite supports +only [a limited subset of ALTER TABLE](https://www.sqlite.org/lang_altertable.html). + Removing a foreign key is easy as well: ```ruby @@ -923,9 +926,10 @@ your database schema. It tends to be faster and less error prone to create a new instance of your application's database by loading the schema file via `rails db:schema:load` -than it is to replay the entire migration history. Old migrations may fail to -apply correctly if those migrations use changing external dependencies or rely -on application code which evolves separately from your migrations. +than it is to replay the entire migration history. +[Old migrations](#old-migrations) may fail to apply correctly if those +migrations use changing external dependencies or rely on application code which +evolves separately from your migrations. Schema files are also useful if you want a quick look at what attributes an Active Record object has. This information is not in the model's code and is @@ -1042,3 +1046,21 @@ end This is generally a much cleaner way to set up the database of a blank application. + +Old Migrations +-------------- + +The `db/schema.rb` or `db/structure.sql` is a snapshot of the current state of your +database and is the authoritative source for rebuilding that database. This +makes it possible to delete old migration files. + +When you delete migration files in the `db/migrate/` directory, any environment +where `rails db:migrate` was run when those files still existed will hold a reference +to the migration timestamp specific to them inside an internal Rails database +table named `schema_migrations`. This table is used to keep track of whether +migrations have been executed in a specific environment. + +If you run the `rails db:migrate:status` command, which displays the status +(up or down) of each migration, you should see `********** NO FILE **********` +displayed next to any deleted migration file which was once executed on a +specific environment but can no longer be found in the `db/migrate/` directory. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index a2890b9b7a..02055e59f0 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1261,7 +1261,7 @@ articles, all the articles would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned. -NOTE: If an association is eager loaded as part of a join, any fields from a custom select clause will not present be on the loaded models. +NOTE: If an association is eager loaded as part of a join, any fields from a custom select clause will not be present on the loaded models. This is because it is ambiguous whether they should appear on the parent record, or the child. Scopes @@ -1277,16 +1277,6 @@ class Article < ApplicationRecord end ``` -This is exactly the same as defining a class method, and which you use is a matter of personal preference: - -```ruby -class Article < ApplicationRecord - def self.published - where(published: true) - end -end -``` - Scopes are also chainable within scopes: ```ruby diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 25f6f7bd2c..0fda7c5cfd 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -845,9 +845,9 @@ class Person < ApplicationRecord end ``` -You can also use `on:` to define custom context. -Custom contexts need to be triggered explicitly -by passing name of the context to `valid?`, `invalid?` or `save`. +You can also use `on:` to define custom contexts. Custom contexts need to be +triggered explicitly by passing the name of the context to `valid?`, +`invalid?`, or `save`. ```ruby class Person < ApplicationRecord @@ -855,14 +855,32 @@ class Person < ApplicationRecord validates :age, numericality: true, on: :account_setup end -person = Person.new +person = Person.new(age: 'thirty-three') +person.valid? # => true +person.valid?(:account_setup) # => false +person.errors.messages + # => {:email=>["has already been taken"], :age=>["is not a number"]} ``` -`person.valid?(:account_setup)` executes both the validations -without saving the model. And `person.save(context: :account_setup)` -validates `person` in `account_setup` context before saving. -On explicit triggers, model is validated by -validations of only that context and validations without context. +`person.valid?(:account_setup)` executes both the validations without saving +the model. `person.save(context: :account_setup)` validates `person` in the +`account_setup` context before saving. + +When triggered by an explicit context, validations are run for that context, +as well as any validations _without_ a context. + +```ruby +class Person < ApplicationRecord + validates :email, uniqueness: true, on: :account_setup + validates :age, numericality: true, on: :account_setup + validates :name, presence: true +end + +person = Person.new +person.valid?(:account_setup) # => false +person.errors.messages + # => {:email=>["has already been taken"], :age=>["is not a number"], :name=>["can't be blank"]} +``` Strict Validations ------------------ @@ -928,6 +946,13 @@ class Account < ApplicationRecord end ``` +As `Lambdas` are a type of `Proc`, they can also be used to write inline +conditions in a shorter way. + +```ruby +validates :password, confirmation: true, unless: -> { password.blank? } +``` + ### Grouping Conditional validations Sometimes it is useful to have multiple validations use one condition. It can diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index 6933717c2b..d5387219f5 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -58,6 +58,8 @@ amazon: service: S3 access_key_id: "" secret_access_key: "" + bucket: "" + region: "" # e.g. 'us-east-1' ``` Tell Active Storage which service to use by setting @@ -80,6 +82,14 @@ To use the Amazon S3 service in production, you add the following to config.active_storage.service = :amazon ``` +To use the test service when testing, you add the following to +`config/environments/test.rb`: + +```ruby +# Store uploaded files on the local file system in a temporary directory. +config.active_storage.service = :test +``` + Continue reading for more information on the built-in service adapters (e.g. `Disk` and `S3`) and the configuration they require. @@ -174,7 +184,7 @@ google: Add the [`google-cloud-storage`](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-storage) gem to your `Gemfile`: ```ruby -gem "google-cloud-storage", "~> 1.8", require: false +gem "google-cloud-storage", "~> 1.11", require: false ``` ### Mirror Service diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index dfd21915b0..6b0554bb5f 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -590,9 +590,9 @@ NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. ### Parents -#### `parent` +#### `module_parent` -The `parent` method on a nested named module returns the module that contains its corresponding constant: +The `module_parent` method on a nested named module returns the module that contains its corresponding constant: ```ruby module X @@ -603,19 +603,19 @@ module X end M = X::Y::Z -X::Y::Z.parent # => X::Y -M.parent # => X::Y +X::Y::Z.module_parent # => X::Y +M.module_parent # => X::Y ``` -If the module is anonymous or belongs to the top-level, `parent` returns `Object`. +If the module is anonymous or belongs to the top-level, `module_parent` returns `Object`. -WARNING: Note that in that case `parent_name` returns `nil`. +WARNING: Note that in that case `module_parent_name` returns `nil`. NOTE: Defined in `active_support/core_ext/module/introspection.rb`. -#### `parent_name` +#### `module_parent_name` -The `parent_name` method on a nested named module returns the fully qualified name of the module that contains its corresponding constant: +The `module_parent_name` method on a nested named module returns the fully qualified name of the module that contains its corresponding constant: ```ruby module X @@ -626,19 +626,19 @@ module X end M = X::Y::Z -X::Y::Z.parent_name # => "X::Y" -M.parent_name # => "X::Y" +X::Y::Z.module_parent_name # => "X::Y" +M.module_parent_name # => "X::Y" ``` -For top-level or anonymous modules `parent_name` returns `nil`. +For top-level or anonymous modules `module_parent_name` returns `nil`. -WARNING: Note that in that case `parent` returns `Object`. +WARNING: Note that in that case `module_parent` returns `Object`. NOTE: Defined in `active_support/core_ext/module/introspection.rb`. -#### `parents` +#### `module_parents` -The method `parents` calls `parent` on the receiver and upwards until `Object` is reached. The chain is returned in an array, from bottom to top: +The method `module_parents` calls `module_parent` on the receiver and upwards until `Object` is reached. The chain is returned in an array, from bottom to top: ```ruby module X @@ -649,8 +649,8 @@ module X end M = X::Y::Z -X::Y::Z.parents # => [X::Y, X, Object] -M.parents # => [X::Y, X, Object] +X::Y::Z.module_parents # => [X::Y, X, Object] +M.module_parents # => [X::Y, X, Object] ``` NOTE: Defined in `active_support/core_ext/module/introspection.rb`. @@ -2156,6 +2156,19 @@ This method is an alias of `Array#<<`. NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. +### Extracting + +The method `extract!` removes and returns the elements for which the block returns a true value. +If no block is given, an Enumerator is returned instead. + +```ruby +numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9] +numbers # => [0, 2, 4, 6, 8] +``` + +NOTE: Defined in `active_support/core_ext/array/extract.rb`. + ### Options Extraction When the last argument in a method call is a hash, except perhaps for a `&block` argument, Ruby allows you to omit the brackets: diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 3568c47dd8..64db141381 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -319,17 +319,18 @@ Action Mailer ### deliver.action_mailer -| Key | Value | -| ------------- | -------------------------------------------- | -| `:mailer` | Name of the mailer class | -| `:message_id` | ID of the message, generated by the Mail gem | -| `:subject` | Subject of the mail | -| `:to` | To address(es) of the mail | -| `:from` | From address of the mail | -| `:bcc` | BCC addresses of the mail | -| `:cc` | CC addresses of the mail | -| `:date` | Date of the mail | -| `:mail` | The encoded form of the mail | +| Key | Value | +| --------------------- | ---------------------------------------------------- | +| `:mailer` | Name of the mailer class | +| `:message_id` | ID of the message, generated by the Mail gem | +| `:subject` | Subject of the mail | +| `:to` | To address(es) of the mail | +| `:from` | From address of the mail | +| `:bcc` | BCC addresses of the mail | +| `:cc` | CC addresses of the mail | +| `:date` | Date of the mail | +| `:mail` | The encoded form of the mail | +| `:perform_deliveries` | Whether delivery of this message is performed or not | ```ruby { @@ -339,7 +340,8 @@ Action Mailer to: ["users@rails.com", "dhh@rails.com"], from: ["me@rails.com"], date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity + mail: "...", # omitted for brevity + perform_deliveries: true } ``` @@ -458,6 +460,15 @@ Active Job | `:adapter` | QueueAdapter object processing the job | | `:job` | Job object | +### enqueue_retry.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:job` | Job object | +| `:adapter` | QueueAdapter object processing the job | +| `:error` | The error that caused the retry | +| `:wait` | The delay of the retry | + ### perform_start.active_job | Key | Value | @@ -472,6 +483,22 @@ Active Job | `:adapter` | QueueAdapter object processing the job | | `:job` | Job object | +### retry_stopped.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | +| `:error` | The error that caused the retry | + +### discard.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | +| `:error` | The error that caused the discard | + Action Cable ------------ @@ -596,7 +623,7 @@ The block receives the following arguments: * The name of the event * Time when it started * Time when it finished -* A unique ID for this event +* A unique ID for the instrumenter that fired the event * The payload (described in previous sections) ```ruby @@ -645,7 +672,8 @@ Creating custom events Adding your own events is easy as well. `ActiveSupport::Notifications` will take care of all the heavy lifting for you. Simply call `instrument` with a `name`, `payload` and a block. The notification will be sent after the block returns. `ActiveSupport` will generate the start and end times -as well as the unique ID. All data passed into the `instrument` call will make it into the payload. +and add the instrumenter's unique ID. All data passed into the `instrument` call will make +it into the payload. Here's an example: diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 6efd9296dc..b6ee7354f9 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -53,7 +53,7 @@ Documentation has to be concise but comprehensive. Explore and document edge cas The proper names of Rails components have a space in between the words, like "Active Support". `ActiveRecord` is a Ruby module, whereas Active Record is an ORM. All Rails documentation should consistently refer to Rails components by their proper name, and if in your next blog post or presentation you remember this tidbit and take it into account that'd be phenomenal. -Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. When in doubt, please have a look at some authoritative source like their official documentation. +Spell names correctly: Arel, minitest, RSpec, HTML, MySQL, JavaScript, ERB. When in doubt, please have a look at some authoritative source like their official documentation. Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index bf046a3341..500e230ff9 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -184,9 +184,8 @@ the file `scaffolds.css` (or `scaffolds.scss` if `sass-rails` is in the `Gemfile`.) For example, if you generate a `ProjectsController`, Rails will also add a new -file at `app/assets/javascripts/projects.coffee` and another at -`app/assets/stylesheets/projects.scss`. By default these files will be ready -to use by your application immediately using the `require_tree` directive. See +file at `app/assets/stylesheets/projects.scss`. By default these files will be +ready to use by your application immediately using the `require_tree` directive. See [Manifest Files and Directives](#manifest-files-and-directives) for more details on require_tree. @@ -234,11 +233,6 @@ code for JavaScript plugins and CSS frameworks. Keep in mind that third party code with references to other files also processed by the asset Pipeline (images, stylesheets, etc.), will need to be rewritten to use helpers like `asset_path`. -WARNING: If you are upgrading from Rails 3, please take into account that assets -under `lib/assets` or `vendor/assets` are available for inclusion via the -application manifests but no longer part of the precompile array. See -[Precompiling Assets](#precompiling-assets) for guidance. - #### Search Paths When a file is referenced from a manifest or a helper, Sprockets searches the @@ -1235,60 +1229,3 @@ it as a preprocessor for your mime type. Sprockets.register_preprocessor 'text/css', AddComment ``` -Upgrading from Old Versions of Rails ------------------------------------- - -There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is -moving the files from `public/` to the new locations. See [Asset -Organization](#asset-organization) above for guidance on the correct locations -for different file types. - -Next is updating the various environment files with the correct default -options. - -In `application.rb`: - -```ruby -# Version of your assets, change this if you want to expire all your assets -config.assets.version = '1.0' - -# Change the path that assets are served from config.assets.prefix = "/assets" -``` - -In `development.rb`: - -```ruby -# Expands the lines which load the assets -config.assets.debug = true -``` - -And in `production.rb`: - -```ruby -# Choose the compressors to use (if any) -config.assets.js_compressor = :uglifier -# config.assets.css_compressor = :yui - -# Don't fallback to assets pipeline if a precompiled asset is missed -config.assets.compile = false - -# Generate digests for assets URLs. -config.assets.digest = true - -# Precompile additional assets (application.js, application.css, and all -# non-JS/CSS are already added) -# config.assets.precompile += %w( admin.js admin.css ) -``` - -Rails 4 and above no longer set default config values for Sprockets in `test.rb`, so -`test.rb` now requires Sprockets configuration. The old defaults in the test -environment are: `config.assets.compile = true`, `config.assets.compress = false`, -`config.assets.debug = false` and `config.assets.digest = false`. - -The following should also be added to your `Gemfile`: - -```ruby -gem 'sass-rails', "~> 3.2.3" -gem 'coffee-rails', "~> 3.2.1" -gem 'uglifier' -``` diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 008c7345e9..f75517c5ab 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -109,7 +109,7 @@ class CreateBooks < ActiveRecord::Migration[5.0] end create_table :books do |t| - t.belongs_to :author, index: true + t.belongs_to :author t.datetime :published_at t.timestamps end @@ -140,7 +140,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0] end create_table :accounts do |t| - t.belongs_to :supplier, index: true + t.belongs_to :supplier t.string :account_number t.timestamps end @@ -184,7 +184,7 @@ class CreateAuthors < ActiveRecord::Migration[5.0] end create_table :books do |t| - t.belongs_to :author, index: true + t.belongs_to :author t.datetime :published_at t.timestamps end @@ -231,8 +231,8 @@ class CreateAppointments < ActiveRecord::Migration[5.0] end create_table :appointments do |t| - t.belongs_to :physician, index: true - t.belongs_to :patient, index: true + t.belongs_to :physician + t.belongs_to :patient t.datetime :appointment_date t.timestamps end @@ -312,13 +312,13 @@ class CreateAccountHistories < ActiveRecord::Migration[5.0] end create_table :accounts do |t| - t.belongs_to :supplier, index: true + t.belongs_to :supplier t.string :account_number t.timestamps end create_table :account_histories do |t| - t.belongs_to :account, index: true + t.belongs_to :account t.integer :credit_rating t.timestamps end @@ -358,8 +358,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration[5.0] end create_table :assemblies_parts, id: false do |t| - t.belongs_to :assembly, index: true - t.belongs_to :part, index: true + t.belongs_to :assembly + t.belongs_to :part end end end @@ -487,7 +487,7 @@ class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name - t.references :imageable, polymorphic: true, index: true + t.references :imageable, polymorphic: true t.timestamps end end @@ -517,7 +517,7 @@ In your migrations/schema, you will add a references column to the model itself. class CreateEmployees < ActiveRecord::Migration[5.0] def change create_table :employees do |t| - t.references :manager, index: true + t.references :manager t.timestamps end end @@ -868,7 +868,7 @@ While Rails uses intelligent defaults that will work well in most situations, th ```ruby class Book < ApplicationRecord - belongs_to :author, dependent: :destroy, + belongs_to :author, touch: :books_updated_at, counter_cache: true end ``` @@ -1048,8 +1048,7 @@ There may be times when you wish to customize the query used by `belongs_to`. Su ```ruby class Book < ApplicationRecord - belongs_to :author, -> { where active: true }, - dependent: :destroy + belongs_to :author, -> { where active: true } end ``` @@ -1075,13 +1074,13 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class LineItem < ApplicationRecord +class Chapter < ApplicationRecord belongs_to :book end class Book < ApplicationRecord belongs_to :author - has_many :line_items + has_many :chapters end class Author < ApplicationRecord @@ -1089,16 +1088,16 @@ class Author < ApplicationRecord end ``` -If you frequently retrieve authors directly from line items (`@line_item.book.author`), then you can make your code somewhat more efficient by including authors in the association from line items to books: +If you frequently retrieve authors directly from chapters (`@chapter.book.author`), then you can make your code somewhat more efficient by including authors in the association from chapters to books: ```ruby -class LineItem < ApplicationRecord +class Chapter < ApplicationRecord belongs_to :book, -> { includes :author } end class Book < ApplicationRecord belongs_to :author - has_many :line_items + has_many :chapters end class Author < ApplicationRecord @@ -1259,7 +1258,7 @@ Controls what happens to the associated object when its owner is destroyed: * `:destroy` causes the associated object to also be destroyed * `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute) * `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed. -* `:restrict_with_exception` causes an exception to be raised if there is an associated record +* `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there is an associated record * `:restrict_with_error` causes an error to be added to the owner if there is an associated object It's necessary not to set or leave `:nullify` option for those associations @@ -1660,7 +1659,7 @@ Controls what happens to the associated objects when their owner is destroyed: * `:destroy` causes all the associated objects to also be destroyed * `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute) * `:nullify` causes the foreign keys to be set to `NULL`. Callbacks are not executed. -* `:restrict_with_exception` causes an exception to be raised if there are any associated records +* `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there are any associated records * `:restrict_with_error` causes an error to be added to the owner if there are any associated objects ##### `:foreign_key` @@ -1779,8 +1778,8 @@ The `group` method supplies an attribute name to group the result set by, using ```ruby class Author < ApplicationRecord - has_many :line_items, -> { group 'books.id' }, - through: :books + has_many :chapters, -> { group 'books.id' }, + through: :books end ``` @@ -1795,27 +1794,27 @@ end class Book < ApplicationRecord belongs_to :author - has_many :line_items + has_many :chapters end -class LineItem < ApplicationRecord +class Chapter < ApplicationRecord belongs_to :book end ``` -If you frequently retrieve line items directly from authors (`@author.books.line_items`), then you can make your code somewhat more efficient by including line items in the association from authors to books: +If you frequently retrieve chapters directly from authors (`@author.books.chapters`), then you can make your code somewhat more efficient by including chapters in the association from authors to books: ```ruby class Author < ApplicationRecord - has_many :books, -> { includes :line_items } + has_many :books, -> { includes :chapters } end class Book < ApplicationRecord belongs_to :author - has_many :line_items + has_many :chapters end -class LineItem < ApplicationRecord +class Chapter < ApplicationRecord belongs_to :book end ``` diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 6298651e4a..b3f923a017 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -1350,3 +1350,34 @@ With the [Spring](https://github.com/rails/spring) pre-loader (included with new Occasionally you may need to explicitly eager_load by using `Rails .application.eager_load!` in the setup of your tests -- this might occur if your [tests involve multithreading](https://stackoverflow.com/questions/25796409/in-rails-how-can-i-eager-load-all-code-before-a-specific-rspec-test). + +## Troubleshooting + +### Tracing Autoloads + +Active Support is able to report constants as they are autoloaded. To enable these traces in a Rails application, put the following two lines in some initializer: + +```ruby +ActiveSupport::Dependencies.logger = Rails.logger +ActiveSupport::Dependencies.verbose = true +``` + +### Where is a Given Autoload Triggered? + +If constant `Foo` is being autoloaded, and you'd like to know where is that autoload coming from, just throw + +```ruby +puts caller +``` + +at the top of `foo.rb` and inspect the printed stack trace. + +### Which Constants Have Been Autoloaded? + +At any given time, + +```ruby +ActiveSupport::Dependencies.autoloaded_constants +``` + +has the collection of constants that have been autoloaded so far. diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 8aaa71c557..321eee637f 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -362,7 +362,7 @@ This class provides the foundation for interacting with the cache in Rails. This The main methods to call are `read`, `write`, `delete`, `exist?`, and `fetch`. The fetch method takes a block and will either return an existing value from the cache, or evaluate the block and write the result to the cache if no value exists. -There are some common options used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries. +There are some common options that can be used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries. * `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications. @@ -370,7 +370,7 @@ There are some common options used by all cache implementations. These can be pa * `:compress_threshold` - Defaults to 1kB. Cache entries larger than this threshold, specified in bytes, are compressed. -* `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache. +* `:expires_in` - This option sets an expiration time in seconds for the cache entry, if the cache store supports it, when it will be automatically removed from the cache. * `:race_condition_ttl` - This option is used in conjunction with the `:expires_in` option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It's a good practice to set this value if you use the `:expires_in` option. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 2f07417316..5fd3ad17de 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -185,7 +185,7 @@ The controller generator is expecting parameters in the form of `generate contro ```bash $ rails generate controller Greetings hello create app/controllers/greetings_controller.rb - route get "greetings/hello" + route get 'greetings/hello' invoke erb create app/views/greetings create app/views/greetings/hello.html.erb @@ -193,9 +193,8 @@ $ rails generate controller Greetings hello create test/controllers/greetings_controller_test.rb invoke helper create app/helpers/greetings_helper.rb + invoke test_unit invoke assets - invoke coffee - create app/assets/javascripts/greetings.coffee invoke scss create app/assets/stylesheets/greetings.scss ``` @@ -546,7 +545,7 @@ vendor/tools.rb: INFO: A good description of unit testing in Rails is given in [A Guide to Testing Rails Applications](testing.html) -Rails comes with a test suite called Minitest. Rails owes its stability to the use of tests. The commands available in the `test:` namespace helps in running the different tests you will hopefully write. +Rails comes with a test framework called minitest. Rails owes its stability to the use of tests. The commands available in the `test:` namespace helps in running the different tests you will hopefully write. ### `rails tmp:` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 6e4f1f9648..d03943ba4a 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -69,7 +69,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such * `config.beginning_of_week` sets the default beginning of week for the application. Accepts a valid week day symbol (e.g. `:monday`). -* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store`. +* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, `:redis_cache_store`, or an object that implements the cache API. Defaults to `:file_store`. * `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to `true`. @@ -104,7 +104,7 @@ application. Accepts a valid week day symbol (e.g. `:monday`). * `config.filter_parameters` used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card -numbers. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. +numbers. It also filters out sensitive values of database columns when call `#inspect` on an Active Record object. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. * `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://api.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. @@ -119,12 +119,11 @@ defaults to `:debug` for all environments. The available log levels are: `:debug * `config.logger` is the logger that will be used for `Rails.logger` and any related Rails logging such as `ActiveRecord::Base.logger`. It defaults to an instance of `ActiveSupport::TaggedLogging` that wraps an instance of `ActiveSupport::Logger` which outputs a log to the `log/` directory. You can supply a custom logger, to get full compatibility you must follow these guidelines: * To support a formatter, you must manually assign a formatter from the `config.log_formatter` value to the logger. * To support tagged logs, the log instance must be wrapped with `ActiveSupport::TaggedLogging`. - * To support silencing, the logger must include `LoggerSilence` and `ActiveSupport::LoggerThreadSafeLevel` modules. The `ActiveSupport::Logger` class already includes these modules. + * To support silencing, the logger must include `ActiveSupport::LoggerSilence` module. The `ActiveSupport::Logger` class already includes these modules. ```ruby class MyLogger < ::Logger - include ActiveSupport::LoggerThreadSafeLevel - include LoggerSilence + include ActiveSupport::LoggerSilence end mylogger = MyLogger.new(STDOUT) @@ -201,8 +200,6 @@ The full set of methods that can be used in this block are as follows: * `helper` defines whether or not to generate helpers. Defaults to `true`. * `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`. * `system_tests` defines which integration tool to use to generate system tests. Defaults to `:test_unit`. -* `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`. -* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`. * `orm` defines which orm to use. Defaults to `false` and will use Active Record by default. * `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`. * `resource_route` defines whether a resource route definition should be generated @@ -211,7 +208,7 @@ The full set of methods that can be used in this block are as follows: * `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`. * `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`. * `scaffold_stylesheet` creates `scaffold.css` when generating a scaffolded resource. Defaults to `true`. -* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default. +* `test_framework` defines which test framework to use. Defaults to `false` and will use minitest by default. * `template_engine` defines which template engine to use, such as ERB or Haml. Defaults to `:erb`. ### Configuring Middleware @@ -275,7 +272,7 @@ config.middleware.delete Rack::MethodOverride All these configuration options are delegated to the `I18n` library. -* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application. +* `config.i18n.available_locales` defines the permitted available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application. * `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`. @@ -444,7 +441,7 @@ The schema dumper adds two additional configuration options: * `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments. -* `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`. +* `config.action_controller.always_permitted_parameters` sets a list of permitted parameters that are permitted by default. The default values are `['controller', 'action']`. * `config.action_controller.enable_fragment_cache_logging` determines whether to log fragment cache reads and writes in verbose format as follows: @@ -516,6 +513,9 @@ Defaults to `'signed cookie'`. signed and encrypted cookies use the AES-256-GCM cipher or the older AES-256-CBC cipher. It defaults to `true`. +* `config.action_dispatch.use_cookies_with_metadata` enables writing + cookies with the purpose and expiry metadata embedded. It defaults to `true`. + * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) for more information. It defaults to `true`. @@ -829,6 +829,14 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla The default is 5 minutes. +* `config.active_storage.routes_prefix` can be used to set the route prefix for the routes served by Active Storage. Accepts a string that will be prepended to the generated routes. + + ```ruby + config.active_storage.routes_prefix = '/files' + ``` + + The default is `/rails/active_storage` + ### Configuring a Database Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. @@ -908,7 +916,15 @@ $ echo $DATABASE_URL postgresql://localhost/my_database $ rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} +#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28> + +$ rails runner 'puts ActiveRecord::Base.configurations.inspect' +#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0 + @env_name="development", @spec_name="primary", + @config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost"} + @url="postgresql://localhost/my_database"> + ] ``` Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`. @@ -925,7 +941,15 @@ $ echo $DATABASE_URL postgresql://localhost/my_database $ rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} +#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28> + +$ rails runner 'puts ActiveRecord::Base.configurations.inspect' +#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0 + @env_name="development", @spec_name="primary", + @config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost", "pool"=>5} + @url="postgresql://localhost/my_database"> + ] ``` Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins. @@ -941,7 +965,15 @@ $ echo $DATABASE_URL postgresql://localhost/my_database $ rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}} +#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28> + +$ rails runner 'puts ActiveRecord::Base.configurations.inspect' +#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0 + @env_name="development", @spec_name="primary", + @config={"adapter"=>"sqlite3", "database"=>"NOT_my_database"} + @url="sqlite3:NOT_my_database"> + ] ``` Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name. @@ -979,7 +1011,7 @@ If you choose to use MySQL or MariaDB instead of the shipped SQLite3 database, y ```yaml development: adapter: mysql2 - encoding: utf8 + encoding: utf8mb4 database: blog_development pool: 5 username: root @@ -989,6 +1021,16 @@ development: If your development database has a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate. +NOTE: If your MySQL version is 5.5 or 5.6 and want to use the `utf8mb4` character set by default, please configure your MySQL server to support the longer key prefix by enabling `innodb_large_prefix` system variable. + +Advisory Locks are enabled by default on MySQL and are used to make database migrations concurrent safe. You can disable advisory locks by setting `advisory_locks` to `false`: + +```yaml +production: + adapter: mysql2 + advisory_locks: false +``` + #### Configuring a PostgreSQL Database If you choose to use PostgreSQL, your `config/database.yml` will be customized to use PostgreSQL databases: @@ -1001,12 +1043,13 @@ development: pool: 5 ``` -Prepared Statements are enabled by default on PostgreSQL. You can disable prepared statements by setting `prepared_statements` to `false`: +By default Active Record uses database features like prepared statements and advisory locks. You might need to disable those features if you're using an external connection pooler like PgBouncer: ```yaml production: adapter: postgresql prepared_statements: false + advisory_locks: false ``` If enabled, Active Record will create up to `1000` prepared statements per database connection by default. To modify this behavior you can set `statement_limit` to a different value: diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 3147b00f3b..709a5146e9 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -135,7 +135,7 @@ learn about Ruby on Rails, and the API, which serves as a reference. You can help improve the Rails guides by making them more coherent, consistent, or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails. To do so, make changes to Rails guides source files (located [here](https://github.com/rails/rails/tree/master/guides/source) on GitHub). Then open a pull request to apply your -changes to master branch. +changes to the master branch. When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html). @@ -239,7 +239,6 @@ Now get busy and add/edit code. You're on your branch now, so you can write what * Include tests that fail without your code, and pass with it. * Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution. - TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted (read more about [our rationales behind this decision](https://github.com/rails/rails/pull/13771#issuecomment-32746700)). #### Follow the Coding Conventions @@ -254,12 +253,24 @@ Rails follows a simple set of coding style conventions: * Prefer class << self over self.method for class methods. * Use `my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. * Use `a = b` and not `a=b`. -* Use assert_not methods instead of refute. +* Use assert\_not methods instead of refute. * Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks. * Follow the conventions in the source you see used already. The above are guidelines - please use your best judgment in using them. +Additionally, we have [RuboCop](https://www.rubocop.org/) rules defined to codify some of our coding conventions. You can run RuboCop locally against the file that you have modified before submitting a pull request: + +```bash +$ rubocop actionpack/lib/action_controller/metal/strong_parameters.rb +Inspecting 1 file +. + +1 file inspected, no offenses detected +``` + +For `rails-ujs` CoffeeScript and JavaScript files, you can run `npm run lint` in `actionview` folder. + ### Benchmark Your Code For changes that might have an impact on performance, please benchmark your @@ -313,6 +324,26 @@ $ cd actionmailer $ bundle exec rake test ``` +#### For a Specific Directory + +If you want to run the tests located in a specific directory use the `TEST_DIR` +environment variable. For example, this will run the tests in the +`railties/test/generators` directory only: + +```bash +$ cd railties +$ TEST_DIR=generators bundle exec rake test +``` + +#### For a Specific File + +You can run the tests for a particular file by using: + +```bash +$ cd actionpack +$ bundle exec ruby -w -Itest test/template/form_helper_test.rb +``` + #### Running a Single Test You can run a single test through ruby. For instance: @@ -322,8 +353,27 @@ $ cd actionmailer $ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` -The `-n` option allows you to run a single method instead of the whole -file. +The `-n` option allows you to run a single method instead of the whole file. + +#### Running tests with a specific seed + +Test execution is randomized with a randomization seed. If you are experiencing random +test failures you can more accurately reproduce a failing test scenario by specifically +setting the randomization seed. + +Running all tests for a component: + +```bash +$ cd actionmailer +$ SEED=15002 bundle exec rake test +``` + +Running a single test file: + +```bash +$ cd actionmailer +$ SEED=15002 bundle exec ruby -w -Itest test/mail_layout_test.rb +``` #### Testing Active Record @@ -477,18 +527,10 @@ Navigate to the Rails [GitHub repository](https://github.com/rails/rails) and pr Add the new remote to your local repository on your local machine: ```bash -$ git remote add mine https://github.com/<your user name>/rails.git +$ git remote add fork https://github.com/<your user name>/rails.git ``` -Push to your remote: - -```bash -$ git push mine my_new_branch -``` - -You might have cloned your forked repository into your machine and might want to add the original Rails repository as a remote instead, if that's the case here's what you have to do. - -In the directory you cloned your fork: +You may have cloned your local repository from rails/rails or you may have cloned from your forked repository. To avoid ambiguity the following git commands assume that you have made a "rails" remote that points to rails/rails. ```bash $ git remote add rails https://github.com/rails/rails.git @@ -505,23 +547,17 @@ Merge the new content: ```bash $ git checkout master $ git rebase rails/master +$ git checkout my_new_branch +$ git rebase rails/master ``` Update your fork: ```bash -$ git push origin master +$ git push fork master +$ git push fork my_new_branch ``` -If you want to update another branch: - -```bash -$ git checkout branch_name -$ git rebase rails/branch_name -$ git push origin branch_name -``` - - ### Issue a Pull Request Navigate to the Rails repository you just pushed to (e.g. @@ -571,35 +607,21 @@ branches, squashing makes it easier to revert bad commits, and the git history can be a bit easier to follow. Rails is a large project, and a bunch of extraneous commits can add a lot of noise. -In order to do this, you'll need to have a git remote that points at the main -Rails repository. This is useful anyway, but just in case you don't have it set -up, make sure that you do this first: - -```bash -$ git remote add upstream https://github.com/rails/rails.git -``` - -You can call this remote whatever you'd like, but if you don't use `upstream`, -then change the name to your own in the instructions below. - -Given that your remote branch is called `my_pull_request`, then you can do the -following: - ```bash -$ git fetch upstream -$ git checkout my_pull_request -$ git rebase -i upstream/master +$ git fetch rails +$ git checkout my_new_branch +$ git rebase -i rails/master < Choose 'squash' for all of your commits except the first one. > < Edit the commit message to make sense, and describe all your changes. > -$ git push origin my_pull_request -f +$ git push fork my_new_branch --force-with-lease ``` You should be able to refresh the pull request on GitHub and see that it has been updated. -#### Updating pull request +#### Updating a pull request Sometimes you will be asked to make some changes to the code you have already committed. This can include amending existing commits. In this @@ -609,19 +631,20 @@ you can force push to your branch on GitHub as described earlier in squashing commits section: ```bash -$ git push origin my_pull_request -f +$ git push fork my_new_branch --force-with-lease ``` -This will update the branch and pull request on GitHub with your new code. Do -note that using force push may result in commits being lost on the remote branch; use it with care. - +This will update the branch and pull request on GitHub with your new code. +By force pushing with `--force-with-lease`, git will more safely update +the remote than with a typical `-f`, which can delete work from the remote +that you don't already have. ### Older Versions of Ruby on Rails If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch: ```bash -$ git branch --track 4-0-stable origin/4-0-stable +$ git branch --track 4-0-stable rails/4-0-stable $ git checkout 4-0-stable ``` diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 88d205e1ab..3a383cbd4d 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -186,14 +186,17 @@ end Here's an example of the log generated when this controller action is executed: ``` -Started POST "/articles" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 +Started POST "/articles" for 127.0.0.1 at 2018-10-18 20:09:23 -0400 Processing by ArticlesController#create as HTML - Parameters: {"utf8"=>"✓", "authenticity_token"=>"xhuIbSBFytHCE1agHgvrlKnSVIOGD6jltW2tO+P6a/ACjQ3igjpV4OdbsZjIhC98QizWH9YdKokrqxBCJrtoqQ==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, "commit"=>"Create Article"} -New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>false, "created_at"=>nil, "updated_at"=>nil} + Parameters: {"utf8"=>"✓", "authenticity_token"=>"XLveDrKzF1SwaiNRPTaMtkrsTzedtebPPkmxEFIU0ordLjICSnXsSNfrdMa4ccyBjuGwnnEiQhEoMN6H1Gtz3A==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>"0"}, "commit"=>"Create Article"} +New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>false, "created_at"=>nil, "updated_at"=>nil} Article should be valid: true - (0.1ms) BEGIN - SQL (0.4ms) INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs!!!"], ["published", "f"], ["created_at", "2017-08-20 11:53:10.010435"], ["updated_at", "2017-08-20 11:53:10.010435"]] - (0.3ms) COMMIT + (0.0ms) begin transaction + ↳ app/controllers/articles_controller.rb:31 + Article Create (0.5ms) INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs."], ["published", 0], ["created_at", "2018-10-19 00:09:23.216549"], ["updated_at", "2018-10-19 00:09:23.216549"]] + ↳ app/controllers/articles_controller.rb:31 + (2.3ms) commit transaction + ↳ app/controllers/articles_controller.rb:31 The article was saved and now the user is going to be redirected... Redirected to http://localhost:3000/articles/1 Completed 302 Found in 4ms (ActiveRecord: 0.8ms) @@ -201,6 +204,40 @@ Completed 302 Found in 4ms (ActiveRecord: 0.8ms) Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia. +### Verbose Query Logs + +When looking at database query output in logs, it may not be immediately clear why multiple database queries are triggered when a single method is called: + +``` +irb(main):001:0> Article.pamplemousse + Article Load (0.4ms) SELECT "articles".* FROM "articles" + Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]] + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]] + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]] +=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10"> +``` + +After running `ActiveRecord::Base.verbose_query_logs = true` in the `rails console` session to enable verbose query logs and running the method again, it becomes obvious what single line of code is generating all these discrete database calls: + +``` +irb(main):003:0> Article.pamplemousse + Article Load (0.2ms) SELECT "articles".* FROM "articles" + ↳ app/models/article.rb:5 + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]] + ↳ app/models/article.rb:6 + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]] + ↳ app/models/article.rb:6 + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]] + ↳ app/models/article.rb:6 +=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10"> +``` + +Below each database statement you can see arrows pointing to the specific source filename (and line number) of the method that resulted in a database call. This can help you identify and address performance problems caused by N+1 queries: single database queries that generates multiple additional queries. + +Verbose query logs are enabled by default in the development environment logs after Rails 5.2. + +WARNING: We recommend against using this setting in production environments. It relies on Ruby's `Kernel#caller` method which tends to allocate a lot of memory in order to generate stacktraces of method calls. + ### Tagged Logging When running multi-user, multi-account applications, it's often useful diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 057bcf2c1b..d52946be08 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -8,8 +8,6 @@ This guide covers how to setup an environment for Ruby on Rails core development After reading this guide, you will know: * How to set up your machine for Rails development -* How to run specific groups of unit tests from the Rails test suite -* How the Active Record portion of the Rails test suite operates -------------------------------------------------------------------------------- @@ -43,195 +41,131 @@ $ git clone https://github.com/rails/rails.git $ cd rails ``` -### Set up and Run the Tests +### Install Additional Tools and Services -The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests. +Some Rails tests depend on additional tools that you need to install before running those specific tests. -Install first SQLite3 and its development files for the `sqlite3` gem. On macOS -users are done with: +Here's the list of each gems' additional dependencies: -```bash -$ brew install sqlite3 -``` - -In Ubuntu you're done with just: - -```bash -$ sudo apt-get install sqlite3 libsqlite3-dev -``` - -If you are on Fedora or CentOS, you're done with - -```bash -$ sudo yum install libsqlite3x libsqlite3x-devel -``` - -If you are on Arch Linux, you will need to run: - -```bash -$ sudo pacman -S sqlite -``` - -For FreeBSD users, you're done with: - -```bash -# pkg install sqlite3 -``` - -Or compile the `databases/sqlite3` port. - -Get a recent version of [Bundler](https://bundler.io/) - -```bash -$ gem install bundler -$ gem update bundler -``` - -and run: - -```bash -$ bundle install --without db -``` +* Action Cable depends on Redis +* Active Record depends on SQLite3, MySQL and PostgreSQL +* Active Storage depends on Yarn (additionally Yarn depends on + [Node.js](https://nodejs.org/)), ImageMagick, FFmpeg, muPDF, and on macOS + also XQuartz and Poppler. +* Active Support depends on memcached and Redis +* Railties depend on a JavaScript runtime environment, such as having + [Node.js](https://nodejs.org/) installed. -This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. +Install all the services you need to properly test the full gem you'll be +making changes to. -NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running. +NOTE: Redis' documentation discourage installations with package managers as those are usually outdated. Installing from source and bringing the server up is straight forward and well documented on [Redis' documentation](https://redis.io/download#installation). -You can use [Homebrew](https://brew.sh/) to install memcached on macOS: +NOTE: Active Record tests _must_ pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against single adapter. -```bash -$ brew install memcached -``` +Below you can find instructions on how to install all of the additional +tools for different OSes. -On Ubuntu you can install it with apt-get: +#### macOS -```bash -$ sudo apt-get install memcached -``` +On macOS you can use [Homebrew](https://brew.sh/) to install all of the +additional tools. -Or use yum on Fedora or CentOS: +To install all run: ```bash -$ sudo yum install memcached +$ brew bundle ``` -If you are running on Arch Linux: +You'll also need to start each of the installed services. To list all +available services run: ```bash -$ sudo pacman -S memcached +$ brew services list ``` -For FreeBSD users, you're done with: +You can then start each of the services one by one like this: ```bash -# pkg install memcached +$ brew services start mysql ``` -Alternatively, you can compile the `databases/memcached` port. +Replace `mysql` with the name of the service you want to start. -With the dependencies now installed, you can run the test suite with: +#### Ubuntu -```bash -$ bundle exec rake test -``` - -You can also run tests for a specific component, like Action Pack, by going into its directory and executing the same command: +To install all run: ```bash -$ cd actionpack -$ bundle exec rake test -``` - -If you want to run the tests located in a specific directory use the `TEST_DIR` environment variable. For example, this will run the tests in the `railties/test/generators` directory only: +$ sudo apt-get update +$ sudo apt-get install sqlite3 libsqlite3-dev + mysql-server libmysqlclient-dev + postgresql postgresql-client postgresql-contrib libpq-dev + redis-server memcached imagemagick ffmpeg mupdf mupdf-tools -```bash -$ cd railties -$ TEST_DIR=generators bundle exec rake test +# Install Yarn +$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +$ sudo apt-get install yarn ``` -You can run the tests for a particular file by using: - -```bash -$ cd actionpack -$ bundle exec ruby -Itest test/template/form_helper_test.rb -``` +#### Fedora or CentOS -Or, you can run a single test in a particular file: +To install all run: ```bash -$ cd actionpack -$ bundle exec ruby -Itest path/to/test.rb -n test_name -``` - -### Railties Setup +$ sudo dnf install sqlite-devel sqlite-libs + mysql-server mysql-devel + postgresql-server postgresql-devel + redis memcached imagemagick ffmpeg mupdf -Some Railties tests depend on a JavaScript runtime environment, such as having [Node.js](https://nodejs.org/) installed. - -### Active Record Setup - -Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them. - -WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. - -#### Database Configuration - -The Active Record test suite requires a custom config file: `activerecord/test/config.yml`. An example is provided in `activerecord/test/config.example.yml` which can be copied and used as needed for your environment. - -#### MySQL and PostgreSQL - -To be able to run the suite for MySQL and PostgreSQL we need their gems. Install -first the servers, their client libraries, and their development files. - -On macOS, you can run: - -```bash -$ brew install mysql -$ brew install postgresql +# Install Yarn +# Use this command if you do not have Node.js installed +$ curl --silent --location https://rpm.nodesource.com/setup_8.x | sudo bash - +# If you have Node.js installed, use this command instead +$ curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo +$ sudo dnf install yarn ``` -Follow the instructions given by Homebrew to start these. - -On Ubuntu, just run: - -```bash -$ sudo apt-get install mysql-server libmysqlclient-dev -$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev -``` +#### Arch Linux -On Fedora or CentOS, just run: +To install all run: ```bash -$ sudo yum install mysql-server mysql-devel -$ sudo yum install postgresql-server postgresql-devel +$ sudo pacman -S sqlite + mariadb libmariadbclient mariadb-clients + postgresql postgresql-libs + redis memcached imagemagick ffmpeg mupdf mupdf-tools poppler + yarn +$ sudo systemctl start redis ``` -If you are running Arch Linux, MySQL isn't supported anymore so you will need to -use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)): +NOTE: If you are running Arch Linux, MySQL isn't supported anymore so you will need to +use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)). -```bash -$ sudo pacman -S mariadb libmariadbclient mariadb-clients -$ sudo pacman -S postgresql postgresql-libs -``` +#### FreeBSD -FreeBSD users will have to run the following: +To install all run: ```bash -# pkg install mysql56-client mysql56-server -# pkg install postgresql94-client postgresql94-server +# pkg install sqlite3 + mysql80-client mysql80-server + postgresql11-client postgresql11-server + memcached imagemagick ffmpeg mupdf + yarn +# portmaster databases/redis ``` -Or install them through ports (they are located under the `databases` folder). -If you run into troubles during the installation of MySQL, please see -[the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html). +Or install everyting through ports (these packages are located under the +`databases` folder). -After that, run: +NOTE: If you run into troubles during the installation of MySQL, please see +[the MySQL documentation](https://dev.mysql.com/doc/refman/8.0/en/freebsd-installation.html). -```bash -$ rm .bundle/config -$ bundle install -``` +### Database Configuration -First, we need to delete `.bundle/config` because Bundler remembers in that file that we didn't want to install the "db" group (alternatively you can edit the file). +There are couple of additional steps required to configure database engines +required for running Active Record tests. In order to be able to run the test suite against MySQL you need to create a user named `rails` with privileges on the test databases: @@ -247,13 +181,6 @@ mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.* to 'rails'@'localhost'; ``` -and create the test databases: - -```bash -$ cd activerecord -$ bundle exec rake db:mysql:build -``` - PostgreSQL's authentication works differently. To setup the development environment with your development account, on Linux or BSD, you just have to run: @@ -267,21 +194,24 @@ and for macOS: $ createuser --superuser $USER ``` -Then, you need to create the test databases with: +Then, you need to create the test databases for both MySQL and PostgreSQL with: ```bash $ cd activerecord -$ bundle exec rake db:postgresql:build +$ bundle exec rake db:create ``` -It is possible to build databases for both PostgreSQL and MySQL with: +NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". + +You can also create test databases for each database engine separately: ```bash $ cd activerecord -$ bundle exec rake db:create +$ bundle exec rake db:mysql:build +$ bundle exec rake db:postgresql:build ``` -You can cleanup the databases using: +and you can drop the databases using: ```bash $ cd activerecord @@ -290,117 +220,40 @@ $ bundle exec rake db:drop NOTE: Using the Rake task to create the test databases ensures they have the correct character set and collation. -NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". - If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails. -### Action Cable Setup - -Action Cable uses Redis as its default subscriptions adapter ([read more](action_cable_overview.html#broadcasting)). Thus, in order to have Action Cable's tests passing you need to install and have Redis running. - -#### Install Redis From Source - -Redis' documentation discourage installations with package managers as those are usually outdated. Installing from source and bringing the server up is straight forward and well documented on [Redis' documentation](https://redis.io/download#installation). - -#### Install Redis From Package Manager - -On macOS, you can run: - -```bash -$ brew install redis -``` - -Follow the instructions given by Homebrew to start these. - -On Ubuntu, just run: - -```bash -$ sudo apt-get install redis-server -``` - -On Fedora or CentOS (requires EPEL enabled), just run: - -```bash -$ sudo yum install redis -``` - -If you are running Arch Linux, just run: - -```bash -$ sudo pacman -S redis -$ sudo systemctl start redis -``` - -FreeBSD users will have to run the following: - -```bash -# portmaster databases/redis -``` - -### Active Storage Setup - -When working on Active Storage, it is important to note that you need to -install its JavaScript dependencies while working on that section of the -codebase. In order to install these dependencies, it is necessary to -have Yarn, a Node.js package manager, available on your system. A -prerequisite for installing this package manager is that -[Node.js](https://nodejs.org) is installed. - +### Install JavaScript dependencies -On macOS, you can run: +If you installed Yarn, you will need to install the javascript dependencies: ```bash -brew install yarn +$ cd activestorage +$ yarn install ``` -On Ubuntu, you can run: +### Install Bundler gem -```bash -curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - -echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - -sudo apt-get update && sudo apt-get install yarn -``` - -On Fedora or CentOS, just run: +Get a recent version of [Bundler](https://bundler.io/) ```bash -sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo - -sudo yum install yarn +$ gem install bundler +$ gem update bundler ``` -Finally, after installing Yarn, you will need to run the following -command inside of the `activestorage` directory to install the dependencies: +and run: ```bash -yarn install +$ bundle install ``` -Extracting previews, tested in ActiveStorage's test suite requires third-party -applications, FFmpeg for video and muPDF for PDFs, and on macOS also XQuartz -and Poppler. Without these applications installed, ActiveStorage tests will -raise errors. - -On macOS you can run: +or: ```bash -brew install ffmpeg -brew cask install xquartz -brew install mupdf-tools -brew install poppler +$ bundle install --without db ``` -On Ubuntu, you can run: - -```bash -sudo apt-get update && install ffmpeg -sudo apt-get update && install mupdf mupdf-tools -``` +if you don't need to run Active Record tests. -On Fedora or CentOS, just run: +### Contribute to Rails -```bash -sudo yum install ffmpeg -sudo yum install mupdf -``` +After you've setup everything, read how you can start [contributing](contributing_to_ruby_on_rails.html#running-an-application-against-your-local-branch). diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 4dee34b1e7..25c159d471 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -173,7 +173,7 @@ description: This guide describes the considerations needed and tools available when working directly with concurrency in a Rails application. work_in_progress: true - - name: Contributing to Ruby on Rails + name: Contributions documents: - name: Contributing to Ruby on Rails @@ -184,14 +184,14 @@ url: api_documentation_guidelines.html description: This guide documents the Ruby on Rails API documentation guidelines. - - name: Ruby on Rails Guides Guidelines + name: Guides Guidelines url: ruby_on_rails_guides_guidelines.html description: This guide documents the Ruby on Rails guides guidelines. - - name: Maintenance Policy + name: Policies documents: - - name: Maintenance Policy for Ruby on Rails + name: Maintenance Policy url: maintenance_policy.html description: What versions of Ruby on Rails are currently supported, and when to expect new versions. - @@ -202,46 +202,51 @@ url: upgrading_ruby_on_rails.html description: This guide helps in upgrading applications to latest Ruby on Rails versions. - - name: Ruby on Rails 5.2 Release Notes + name: 6.0 Release Notes + work_in_progress: true + url: 6_0_release_notes.html + description: Release notes for Rails 6.0. + - + name: Version 5.2 - April 2018 url: 5_2_release_notes.html description: Release notes for Rails 5.2. - - name: Ruby on Rails 5.1 Release Notes + name: Version 5.1 - April 2017 url: 5_1_release_notes.html description: Release notes for Rails 5.1. - - name: Ruby on Rails 5.0 Release Notes + name: Version 5.0 - June 2016 url: 5_0_release_notes.html description: Release notes for Rails 5.0. - - name: Ruby on Rails 4.2 Release Notes + name: Version 4.2 - December 2014 url: 4_2_release_notes.html description: Release notes for Rails 4.2. - - name: Ruby on Rails 4.1 Release Notes + name: Version 4.1 - April 2014 url: 4_1_release_notes.html description: Release notes for Rails 4.1. - - name: Ruby on Rails 4.0 Release Notes + name: Version 4.0 - June 2013 url: 4_0_release_notes.html description: Release notes for Rails 4.0. - - name: Ruby on Rails 3.2 Release Notes + name: Version 3.2 - January 2012 url: 3_2_release_notes.html description: Release notes for Rails 3.2. - - name: Ruby on Rails 3.1 Release Notes + name: Version 3.1 - August 2011 url: 3_1_release_notes.html description: Release notes for Rails 3.1. - - name: Ruby on Rails 3.0 Release Notes + name: Version 3.0 - August 2010 url: 3_0_release_notes.html description: Release notes for Rails 3.0. - - name: Ruby on Rails 2.3 Release Notes + name: Version 2.3 - March 2009 url: 2_3_release_notes.html description: Release notes for Rails 2.3. - - name: Ruby on Rails 2.2 Release Notes + name: Version 2.2 - November 2008 url: 2_2_release_notes.html description: Release notes for Rails 2.2. diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index a4f7e6f601..b5e2c49487 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -22,27 +22,25 @@ NOTE: This guide is not intended to be a complete documentation of available for Dealing with Basic Forms ------------------------ -The most basic form helper is `form_tag`. +The main form helper is `form_with`. ```erb -<%= form_tag do %> +<%= form_with do %> Form contents <% end %> ``` -When called without arguments like this, it creates a `<form>` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability): +When called without arguments like this, it creates a form tag which, when submitted, will POST to the current page. For instance, assuming the current page is a home page, the generated HTML will look like this: ```html -<form accept-charset="UTF-8" action="/" method="post"> - <input name="utf8" type="hidden" value="✓" /> +<form accept-charset="UTF-8" action="/" data-remote="true" method="post"> <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" /> Form contents </form> ``` -You'll notice that the HTML contains an `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element with the name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their action is "GET" or "POST". - -The second input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). +You'll notice that the HTML contains an `input` element with type `hidden`. This `input` is important, because non-GET form cannot be successfully submitted without it. +The hidden input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Securing Rails Applications](security.html#cross-site-request-forgery-csrf) guide. ### A Generic Search Form @@ -53,10 +51,10 @@ One of the most basic forms you see on the web is a search form. This form conta * a text input element, and * a submit element. -To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and `submit_tag`, respectively. Like this: +To create this form you will use `form_with`, `label_tag`, `text_field_tag`, and `submit_tag`, respectively. Like this: ```erb -<%= form_tag("/search", method: "get") do %> +<%= form_with(url: "/search", method: "get") do %> <%= label_tag(:q, "Search for:") %> <%= text_field_tag(:q) %> <%= submit_tag("Search") %> @@ -66,37 +64,18 @@ To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/search" method="get"> - <input name="utf8" type="hidden" value="✓" /> +<form accept-charset="UTF-8" action="/search" data-remote="true" method="get"> <label for="q">Search for:</label> <input id="q" name="q" type="text" /> - <input name="commit" type="submit" value="Search" /> + <input name="commit" type="submit" value="Search" data-disable-with="Search" /> </form> ``` -TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. - -Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML. - -IMPORTANT: Always use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action. - -### Multiple Hashes in Form Helper Calls - -The `form_tag` helper accepts 2 arguments: the path for the action and an options hash. This hash specifies the method of form submission and HTML options such as the form element's class. - -As with the `link_to` helper, the path argument doesn't have to be a string; it can be a hash of URL parameters recognizable by Rails' routing mechanism, which will turn the hash into a valid URL. However, since both arguments to `form_tag` are hashes, you can easily run into a problem if you would like to specify both. For instance, let's say you write this: - -```ruby -form_tag(controller: "people", action: "search", method: "get", class: "nifty_form") -# => '<form accept-charset="UTF-8" action="/people/search?method=get&class=nifty_form" method="post">' -``` +TIP: Passing `url: my_specified_path` to `form_with` tells the form where to make the request. However, as explained below, you can also pass ActiveRecord objects to the form. -Here, `method` and `class` are appended to the query string of the generated URL because even though you mean to write two hashes, you really only specified one. So you need to tell Ruby which is which by delimiting the first hash (or both) with curly brackets. This will generate the HTML you expect: +TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. -```ruby -form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form") -# => '<form accept-charset="UTF-8" action="/people/search" method="get" class="nifty_form">' -``` +IMPORTANT: Use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action. ### Helpers for Generating Form Elements @@ -110,7 +89,7 @@ value entered by the user for that field. For example, if the form contains `<%= text_field_tag(:query) %>`, then you would be able to get the value of this field in the controller with `params[:query]`. -When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in [chapter 7 of this guide](#understanding-parameter-naming-conventions). For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html). +When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in chapter [Understanding Parameter Naming Conventions](#understanding-parameter-naming-conventions) of this guide. For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html). #### Checkboxes @@ -142,7 +121,7 @@ Radio buttons, while similar to checkboxes, are controls that specify a set of o <%= radio_button_tag(:age, "child") %> <%= label_tag(:age_child, "I am younger than 21") %> <%= radio_button_tag(:age, "adult") %> -<%= label_tag(:age_adult, "I'm over 21") %> +<%= label_tag(:age_adult, "I am over 21") %> ``` Output: @@ -151,7 +130,7 @@ Output: <input id="age_child" name="age" type="radio" value="child" /> <label for="age_child">I am younger than 21</label> <input id="age_adult" name="age" type="radio" value="adult" /> -<label for="age_adult">I'm over 21</label> +<label for="age_adult">I am over 21</label> ``` As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either `"child"` or `"adult"`. @@ -215,7 +194,7 @@ There is definitely [no shortage of solutions for this](https://github.com/Moder [Modernizr](https://modernizr.com/), which provides a simple way to add functionality based on the presence of detected HTML5 features. -TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging). +TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Securing Rails Applications](security.html#logging) guide. Dealing with Model Objects -------------------------- @@ -233,10 +212,10 @@ For these helpers the first argument is the name of an instance variable and the will produce output similar to ```erb -<input id="person_name" name="person[name]" type="text" value="Henry"/> +<input id="person_name" name="person[name]" type="text" value="Henry" /> ``` -Upon form submission the value entered by the user will be stored in `params[:person][:name]`. The `params[:person]` hash is suitable for passing to `Person.new` or, if `@person` is an instance of Person, `@person.update`. While the name of an attribute is the most common second parameter to these helpers this is not compulsory. In the example above, as long as person objects have a `name` and a `name=` method Rails will be happy. +Upon form submission the value entered by the user will be stored in `params[:person][:name]`. WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object. @@ -244,7 +223,7 @@ Rails provides helpers for displaying the validation errors associated with a mo ### Binding a Form to an Object -While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does. +While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_with` with `:model` does. Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`: @@ -254,10 +233,10 @@ def new end ``` -The corresponding view `app/views/articles/new.html.erb` using `form_for` looks like this: +The corresponding view `app/views/articles/new.html.erb` using `form_with` looks like this: ```erb -<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %> +<%= form_with model: @article, class: "nifty_form" do |f| %> <%= f.text_field :title %> <%= f.text_area :body, size: "60x12" %> <%= f.submit "Create" %> @@ -267,15 +246,15 @@ The corresponding view `app/views/articles/new.html.erb` using `form_for` looks There are a few things to note here: * `@article` is the actual object being edited. -* There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id. -* The `form_for` method yields a **form builder** object (the `f` variable). +* There is a single hash of options. HTML options (except `id` and `class`) are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The scope attribute will be prefixed with underscore on the generated HTML id. +* The `form_with` method yields a **form builder** object (the `f` variable). +* If you wish to direct your form request to a particular url, you would use `form_with url: my_nifty_url_path` instead. To see more in depth options on what `form_with` accepts be sure to [check out the API documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with). * Methods to create form controls are called **on** the form builder object `f`. The resulting HTML is: ```html -<form class="nifty_form" id="new_article" action="/articles" accept-charset="UTF-8" method="post"> - <input name="utf8" type="hidden" value="✓" /> +<form class="nifty_form" action="/articles" accept-charset="UTF-8" data-remote="true" method="post"> <input type="hidden" name="authenticity_token" value="NRkFyRWxdYNfUg7vYxLOp2SLf93lvnl+QwDWorR42Dp6yZXPhHEb6arhDOIWcqGit8jfnrPwL781/xlrzj63TA==" /> <input type="text" name="article[title]" id="article_title" /> <textarea name="article[body]" id="article_body" cols="60" rows="12"></textarea> @@ -283,16 +262,18 @@ The resulting HTML is: </form> ``` -The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the [parameter_names section](#understanding-parameter-naming-conventions). +The object passed as `:model` in `form_with` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in chapter [Understanding Parameter Naming Conventions](#understanding-parameter-naming-conventions) of this guide. + +TIP: Conventionally your inputs will mirror model attributes. However, they don't have to! If there is other information you need you can include it in your form just as with attributes and access it via `params[:article][:my_nifty_non_attribute_input]`. The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example, if you had a `Person` model with an associated `ContactDetail` model, you could create a form for creating both like so: ```erb -<%= form_for @person, url: {action: "create"} do |person_form| %> +<%= form_with model: @person do |person_form| %> <%= person_form.text_field :name %> - <%= fields_for @person.contact_detail do |contact_detail_form| %> + <%= fields_for :contact_detail, @person.contact_detail do |contact_detail_form| %> <%= contact_detail_form.text_field :phone_number %> <% end %> <% end %> @@ -301,15 +282,14 @@ You can create a similar binding without actually creating `<form>` tags with th which produces the following output: ```html -<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post"> - <input name="utf8" type="hidden" value="✓" /> +<form action="/people" accept-charset="UTF-8" data-remote="true" method="post"> <input type="hidden" name="authenticity_token" value="bL13x72pldyDD8bgtkjKQakJCpd4A8JdXGbfksxBDHdf1uC0kCMqe2tvVdUYfidJt0fj3ihC4NxiVHv8GVYxJA==" /> <input type="text" name="person[name]" id="person_name" /> <input type="text" name="contact_detail[phone_number]" id="contact_detail_phone_number" /> </form> ``` -The object yielded by `fields_for` is a form builder like the one yielded by `form_for` (in fact `form_for` calls `fields_for` internally). +The object yielded by `fields_for` is a form builder like the one yielded by `form_with`. ### Relying on Record Identification @@ -319,62 +299,59 @@ The Article model is directly available to users of the application, so - follow resources :articles ``` -TIP: Declaring a resource has a number of side effects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources. +TIP: Declaring a resource has a number of side effects. See [Rails Routing from the Outside In](routing.html#resource-routing-the-rails-default) guide for more information on setting up and using resources. -When dealing with RESTful resources, calls to `form_for` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest: +When dealing with RESTful resources, calls to `form_with` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest: ```ruby ## Creating a new article # long-style: -form_for(@article, url: articles_path) -# same thing, short-style (record identification gets used): -form_for(@article) +form_with(model: @article, url: articles_path) +short-style: +form_with(model: @article) ## Editing an existing article # long-style: -form_for(@article, url: article_path(@article), html: {method: "patch"}) +form_with(model: @article, url: article_path(@article), method: "patch") # short-style: -form_for(@article) +form_with(model: @article) ``` -Notice how the short-style `form_for` invocation is conveniently the same, regardless of the record being new or existing. Record identification is smart enough to figure out if the record is new by asking `record.new_record?`. It also selects the correct path to submit to and the name based on the class of the object. - -Rails will also automatically set the `class` and `id` of the form appropriately: a form creating an article would have `id` and `class` `new_article`. If you were editing the article with id 23, the `class` would be set to `edit_article` and the id to `edit_article_23`. These attributes will be omitted for brevity in the rest of this guide. +Notice how the short-style `form_with` invocation is conveniently the same, regardless of the record being new or existing. Record identification is smart enough to figure out if the record is new by asking `record.new_record?`. It also selects the correct path to submit to, and the name based on the class of the object. -WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify the model name, `:url`, and `:method` explicitly. +WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify `:url`, and `:scope` (the model name) explicitly. #### Dealing with Namespaces -If you have created namespaced routes, `form_for` has a nifty shorthand for that too. If your application has an admin namespace then +If you have created namespaced routes, `form_with` has a nifty shorthand for that too. If your application has an admin namespace then ```ruby -form_for [:admin, @article] +form_with model: [:admin, @article] ``` will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar: ```ruby -form_for [:admin, :management, @article] +form_with model: [:admin, :management, @article] ``` -For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html). +For more information on Rails' routing system and the associated conventions, please see [Rails Routing from the Outside In](routing.html) guide. ### How do forms with PATCH, PUT, or DELETE methods work? -The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. +The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH", "PUT", and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. Rails works around this issue by emulating other methods over POST with a hidden input named `"_method"`, which is set to reflect the desired method: ```ruby -form_tag(search_path, method: "patch") +form_with(url: search_path, method: "patch") ``` -output: +Output: ```html -<form accept-charset="UTF-8" action="/search" method="post"> +<form accept-charset="UTF-8" action="/search" data-remote="true" method="post"> <input name="_method" type="hidden" value="patch" /> - <input name="utf8" type="hidden" value="✓" /> <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> ... </form> @@ -382,6 +359,8 @@ output: When parsing POSTed data, Rails will take into account the special `_method` parameter and act as if the HTTP method was the one specified inside it ("PATCH" in this example). +IMPORTANT: All forms using `form_with` implement `remote: true` by default. These forms will submit data using an XHR (Ajax) request. To disable this include `local: true`. To dive deeper see [Working with JavaScript in Rails](working_with_javascript_in_rails.html#remote-elements) guide. + Making Select Boxes with Ease ----------------------------- @@ -393,8 +372,7 @@ Here is what the markup might look like: <select name="city_id" id="city_id"> <option value="1">Lisbon</option> <option value="2">Madrid</option> - ... - <option value="12">Berlin</option> + <option value="3">Berlin</option> </select> ``` @@ -405,19 +383,21 @@ Here you have a list of cities whose names are presented to the user. Internally The most generic helper is `select_tag`, which - as the name implies - simply generates the `SELECT` tag that encapsulates an options string: ```erb -<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %> +<%= select_tag(:city_id, raw('<option value="1">Lisbon</option><option value="2">Madrid</option><option value="3">Berlin</option>')) %> ``` This is a start, but it doesn't dynamically create the option tags. You can generate option tags with the `options_for_select` helper: ```html+erb -<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %> +<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]]) %> +``` -output: +Output: +```html <option value="1">Lisbon</option> <option value="2">Madrid</option> -... +<option value="3">Berlin</option> ``` The first argument to `options_for_select` is a nested array where each element has two elements: option text (city name) and option value (city id). The option value is what will be submitted to your controller. Often this will be the id of a corresponding database object but this does not have to be the case. @@ -431,48 +411,61 @@ Knowing this, you can combine `select_tag` and `options_for_select` to achieve t `options_for_select` allows you to pre-select an option by passing its value. ```html+erb -<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %> +<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]], 2) %> +``` -output: +Output: +```html <option value="1">Lisbon</option> <option value="2" selected="selected">Madrid</option> -... +<option value="3">Berlin</option> ``` Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option. -WARNING: When `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one, and `multiple` is not true. - You can add arbitrary attributes to the options using hashes: ```html+erb <%= options_for_select( [ ['Lisbon', 1, { 'data-size' => '2.8 million' }], - ['Madrid', 2, { 'data-size' => '3.2 million' }] + ['Madrid', 2, { 'data-size' => '3.2 million' }], + ['Berlin', 3, { 'data-size' => '3.4 million' }] ], 2 ) %> +``` -output: +Output: +```html <option value="1" data-size="2.8 million">Lisbon</option> <option value="2" selected="selected" data-size="3.2 million">Madrid</option> -... +<option value="3" data-size="3.4 million">Berlin</option> ``` -### Select Boxes for Dealing with Models +### Select Boxes for Dealing with Model Objects + +In most cases form controls will be tied to a specific model and as you might expect Rails provides helpers tailored for that purpose. Consistent with other form helpers, when dealing with a model object drop the `_tag` suffix from `select_tag`: -In most cases form controls will be tied to a specific database model and as you might expect Rails provides helpers tailored for that purpose. Consistent with other form helpers, when dealing with models you drop the `_tag` suffix from `select_tag`: +If your controller has defined `@person` and that person's city_id is 2: ```ruby -# controller: @person = Person.new(city_id: 2) ``` ```erb -# view: -<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %> +<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]]) %> +``` + +will produce output similar to + +```html +<select name="person[city_id]" id="person_city_id"> + <option value="1">Lisbon</option> + <option value="2" selected="selected">Madrid</option> + <option value="3">Berlin</option> +</select> ``` Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one - Rails will do this for you by reading from the `@person.city_id` attribute. @@ -480,21 +473,26 @@ Notice that the third parameter, the options array, is the same kind of argument As with other helpers, if you were to use the `select` helper on a form builder scoped to the `@person` object, the syntax would be: ```erb -# select on a form builder -<%= f.select(:city_id, ...) %> +<%= form_with model: @person do |person_form| %> + <%= person_form.select(:city_id, [['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]]) %> +<% end %> ``` You can also pass a block to `select` helper: ```erb -<%= f.select(:city_id) do %> - <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%> - <%= content_tag(:option, c.first, value: c.last) %> +<%= form_with model: @person do |person_form| %> + <%= person_form.select(:city_id) do %> + <% [['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]].each do |c| %> + <%= content_tag(:option, c.first, value: c.last) %> + <% end %> <% end %> <% end %> ``` -WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. +WARNING: If you are using `select` or similar helpers to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. + +WARNING: When `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one, and `multiple` is not true. ### Option Tags from a Collection of Arbitrary Objects @@ -511,7 +509,7 @@ This is a perfectly valid solution, but Rails provides a less verbose alternativ <%= options_from_collection_for_select(City.all, :id, :name) %> ``` -As the name implies, this only generates option tags. To generate a working select box you would need to use it in conjunction with `select_tag`, just as you would with `options_for_select`. When working with model objects, just as `select` combines `select_tag` and `options_for_select`, `collection_select` combines `select_tag` with `options_from_collection_for_select`. +As the name implies, this only generates option tags. To generate a working select box you would need to use `collection_select`: ```erb <%= collection_select(:person, :city_id, City.all, :id, :name) %> @@ -520,16 +518,16 @@ As the name implies, this only generates option tags. To generate a working sele As with other helpers, if you were to use the `collection_select` helper on a form builder scoped to the `@person` object, the syntax would be: ```erb -<%= f.collection_select(:city_id, City.all, :id, :name) %> +<%= form_with model: @person do |person_form| %> + <%= person_form.collection_select(:city_id, City.all, :id, :name) %> +<% end %> ``` -To recap, `options_from_collection_for_select` is to `collection_select` what `options_for_select` is to `select`. - -NOTE: Pairs passed to `options_for_select` should have the name first and the id second, however with `options_from_collection_for_select` the first argument is the value method and the second the text method. +NOTE: Pairs passed to `options_for_select` should have the text first and the value second, however with `options_from_collection_for_select` should have the value method first and the text method second. ### Time Zone and Country Select -To leverage time zone support in Rails, you have to ask your users what time zone they are in. Doing so would require generating select options from a list of pre-defined TimeZone objects using `collection_select`, but you can simply use the `time_zone_select` helper that already wraps this: +To leverage time zone support in Rails, you have to ask your users what time zone they are in. Doing so would require generating select options from a list of pre-defined [`ActiveSupport::TimeZone`](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html) objects using `collection_select`, but you can simply use the `time_zone_select` helper that already wraps this: ```erb <%= time_zone_select(:person, :time_zone) %> @@ -537,21 +535,21 @@ To leverage time zone support in Rails, you have to ask your users what time zon There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-time_zone_options_for_select) to learn about the possible arguments for these two methods. -Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/stefanpenner/country_select). When using this, be aware that the exclusion or inclusion of certain names from the list can be somewhat controversial (and was the reason this functionality was extracted from Rails). +Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/stefanpenner/country_select). Using Date and Time Form Helpers -------------------------------- You can choose not to use the form helpers generating HTML5 date and time input fields and use the alternative date and time helpers. These date and time helpers differ from all the other form helpers in two important respects: -* Dates and times are not representable by a single input element. Instead you have several, one for each component (year, month, day etc.) and so there is no single value in your `params` hash with your date or time. +* Dates and times are not representable by a single input element. Instead, you have several, one for each component (year, month, day etc.) and so there is no single value in your `params` hash with your date or time. * Other helpers use the `_tag` suffix to indicate whether a helper is a barebones helper or one that operates on model objects. With dates and times, `select_date`, `select_time` and `select_datetime` are the barebones helpers, `date_select`, `time_select` and `datetime_select` are the equivalent model object helpers. Both of these families of helpers will create a series of select boxes for the different components (year, month, day etc.). ### Barebones Helpers -The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example: +The `select_*` family of helpers take as their first argument an instance of `Date`, `Time`, or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example: ```erb <%= select_date Date.today, prefix: :start_date %> @@ -560,12 +558,15 @@ The `select_*` family of helpers take as their first argument an instance of `Da outputs (with actual option values omitted for brevity) ```html -<select id="start_date_year" name="start_date[year]"> ... </select> -<select id="start_date_month" name="start_date[month]"> ... </select> -<select id="start_date_day" name="start_date[day]"> ... </select> +<select id="start_date_year" name="start_date[year]"> +</select> +<select id="start_date_month" name="start_date[month]"> +</select> +<select id="start_date_day" name="start_date[day]"> +</select> ``` -The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example: +The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time`, or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example: ```ruby Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i) @@ -585,9 +586,12 @@ The model object helpers for dates and times submit parameters with special name outputs (with actual option values omitted for brevity) ```html -<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select> -<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select> -<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select> +<select id="person_birth_date_1i" name="person[birth_date(1i)]"> +</select> +<select id="person_birth_date_2i" name="person[birth_date(2i)]"> +</select> +<select id="person_birth_date_3i" name="person[birth_date(3i)]"> +</select> ``` which results in a `params` hash like @@ -604,68 +608,60 @@ Both families of helpers use the same core set of functions to generate the indi As a rule of thumb you should be using `date_select` when working with model objects and `select_date` in other cases, such as a search form which filters results by date. -NOTE: In many cases the built-in date pickers are clumsy as they do not aid the user in working out the relationship between the date and the day of the week. - ### Individual Components Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example, "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value. -The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example: +The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time`, or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example: ```erb <%= select_year(2009) %> -<%= select_year(Time.now) %> +<%= select_year(Time.new(2009)) %> ``` -will produce the same output if the current year is 2009 and the value chosen by the user can be retrieved by `params[:date][:year]`. +will produce the same output and the value chosen by the user can be retrieved by `params[:date][:year]`. Uploading Files --------------- -A common task is uploading some sort of file, whether it's a picture of a person or a CSV file containing data to process. The most important thing to remember with file uploads is that the rendered form's encoding **MUST** be set to "multipart/form-data". If you use `form_for`, this is done automatically. If you use `form_tag`, you must set it yourself, as per the following example. +A common task is uploading some sort of file, whether it's a picture of a person or a CSV file containing data to process. The most important thing to remember with file uploads is that the rendered form's enctype attribute **must** be set to "multipart/form-data". If you use `form_with` with `:model`, this is done automatically. If you use `form_with` without `:model`, you must set it yourself, as per the following example. The following two forms both upload a file. ```erb -<%= form_tag({action: :upload}, multipart: true) do %> +<%= form_with(url: {action: :upload}, multipart: true) do %> <%= file_field_tag 'picture' %> <% end %> -<%= form_for @person do |f| %> +<%= form_with model: @person do |f| %> <%= f.file_field :picture %> <% end %> ``` -Rails provides the usual pair of helpers: the barebones `file_field_tag` and the model oriented `file_field`. The only difference with other helpers is that you cannot set a default value for file inputs as this would have no meaning. As you would expect in the first case the uploaded file is in `params[:picture]` and in the second case in `params[:person][:picture]`. +Rails provides the usual pair of helpers: the barebones `file_field_tag` and the model oriented `file_field`. As you would expect in the first case the uploaded file is in `params[:picture]` and in the second case in `params[:person][:picture]`. ### What Gets Uploaded -The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a `StringIO` or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). +The object in the `params` hash is an instance of [`ActionDispatch::Http::UploadedFile`](http://api.rubyonrails.org/classes/ActionDispatch/Http/UploadedFile.html). The following snippet saves the uploaded file in `#{Rails.root}/public/uploads` under the same name as the original file. ```ruby def upload - uploaded_io = params[:person][:picture] - File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file| - file.write(uploaded_io.read) + uploaded_file = params[:picture] + File.open(Rails.root.join('public', 'uploads', uploaded_file.original_filename), 'wb') do |file| + file.write(uploaded_file.read) end end ``` -Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. [Active Storage](https://guides.rubyonrails.org/active_storage_overview.html) is designed to assist with these tasks. - -NOTE: If the user has not selected a file the corresponding parameter will be an empty string. - -### Dealing with Ajax - -Unlike other forms, making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. +Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on Disk, Amazon S3, etc), associating them with models, resizing image files, and generating thumbnails, etc. [Active Storage](active_storage_overview.html) is designed to assist with these tasks. Customizing Form Builders ------------------------- -As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example: +The object yielded by `form_with` and `fields_for` is an instance of [`ActionView::Helpers::FormBuilder`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html). Form builders encapsulate the notion of displaying form elements for a single object. While you can write helpers for your forms in the usual way, you can also create subclass `ActionView::Helpers::FormBuilder` and add the helpers there. For example: ```erb -<%= form_for @person do |f| %> +<%= form_with model: @person do |f| %> <%= text_field_with_label f, :first_name %> <% end %> ``` @@ -673,7 +669,7 @@ As mentioned previously the object yielded by `form_for` and `fields_for` is an can be replaced with ```erb -<%= form_for @person, builder: LabellingFormBuilder do |f| %> +<%= form_with model: @person, builder: LabellingFormBuilder do |f| %> <%= f.text_field :first_name %> <% end %> ``` @@ -688,12 +684,12 @@ class LabellingFormBuilder < ActionView::Helpers::FormBuilder end ``` -If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option: +If you reuse this frequently you could define a `labeled_form_with` helper that automatically applies the `builder: LabellingFormBuilder` option: ```ruby -def labeled_form_for(record, options = {}, &block) +def labeled_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block) options.merge! builder: LabellingFormBuilder - form_for record, options, &block + form_with model: model, scope: scope, url: url, format: format, **options, &block end ``` @@ -703,13 +699,12 @@ The form builder used also determines what happens when you do <%= render partial: f %> ``` -If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead. +If `f` is an instance of `ActionView::Helpers::FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead. Understanding Parameter Naming Conventions ------------------------------------------ -As you've seen in the previous sections, values from forms can be at the top level of the `params` hash or nested in another hash. For example, in a standard `create` -action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes, and so on. +Values from forms can be at the top level of the `params` hash or nested in another hash. For example, in a standard `create` action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes, and so on. Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses. @@ -756,28 +751,31 @@ This would result in `params[:person][:phone_number]` being an array containing We can mix and match these two concepts. One element of a hash might be an array as in the previous example, or you can have an array of hashes. For example, a form might let you create any number of addresses by repeating the following form fragment ```html -<input name="addresses[][line1]" type="text"/> -<input name="addresses[][line2]" type="text"/> -<input name="addresses[][city]" type="text"/> +<input name="person[addresses][][line1]" type="text"/> +<input name="person[addresses][][line2]" type="text"/> +<input name="person[addresses][][city]" type="text"/> +<input name="person[addresses][][line1]" type="text"/> +<input name="person[addresses][][line2]" type="text"/> +<input name="person[addresses][][city]" type="text"/> ``` -This would result in `params[:addresses]` being an array of hashes with keys `line1`, `line2` and `city`. Rails decides to start accumulating values in a new hash whenever it encounters an input name that already exists in the current hash. +This would result in `params[:person][:addresses]` being an array of hashes with keys `line1`, `line2`, and `city`. There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can usually be replaced by hashes; for example, instead of having an array of model objects, one can have a hash of model objects keyed by their id, an array index, or some other parameter. -WARNING: Array parameters do not play well with the `check_box` helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The `check_box` helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. When working with array parameters this duplicate submission will confuse Rails since duplicate input names are how it decides when to start a new array element. It is preferable to either use `check_box_tag` or to use hashes instead of arrays. +WARNING: Array parameters do not play well with the `check_box` helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The `check_box` helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. ### Using Form Helpers -The previous sections did not use the Rails form helpers at all. While you can craft the input names yourself and pass them directly to helpers such as `text_field_tag` Rails also provides higher level support. The two tools at your disposal here are the name parameter to `form_for` and `fields_for` and the `:index` option that helpers take. +The previous sections did not use the Rails form helpers at all. While you can craft the input names yourself and pass them directly to helpers such as `text_field_tag` Rails also provides higher level support. The two tools at your disposal here are the name parameter to `form_with` and `fields_for` and the `:index` option that helpers take. You might want to render a form with a set of edit fields for each of a person's addresses. For example: ```erb -<%= form_for @person do |person_form| %> +<%= form_with model: @person do |person_form| %> <%= person_form.text_field :name %> <% @person.addresses.each do |address| %> - <%= person_form.fields_for address, index: address.id do |address_form|%> + <%= person_form.fields_for address, index: address.id do |address_form| %> <%= address_form.text_field :city %> <% end %> <% end %> @@ -787,7 +785,8 @@ You might want to render a form with a set of edit fields for each of a person's Assuming the person had two addresses, with ids 23 and 45 this would create output similar to this: ```html -<form accept-charset="UTF-8" action="/people/1" class="edit_person" id="edit_person_1" method="post"> +<form accept-charset="UTF-8" action="/people/1" data-remote="true" method="post"> + <input name="_method" type="hidden" value="patch" /> <input id="person_name" name="person[name]" type="text" /> <input id="person_address_23_city" name="person[address][23][city]" type="text" /> <input id="person_address_45_city" name="person[address][45][city]" type="text" /> @@ -812,7 +811,7 @@ To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly: ```erb -<%= fields_for 'person[address][primary]', address, index: address do |address_form| %> +<%= fields_for 'person[address][primary]', address, index: address.id do |address_form| %> <%= address_form.text_field :city %> <% end %> ``` @@ -820,12 +819,12 @@ name (`person[address]` in the previous example) explicitly: will create inputs like ```html -<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" /> +<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="Bologna" /> ``` -As a general rule the final input name is the concatenation of the name given to `fields_for`/`form_for`, the index value, and the name of the attribute. You can also pass an `:index` option directly to helpers such as `text_field`, but it is usually less repetitive to specify this at the form builder level rather than on individual input controls. +As a general rule the final input name is the concatenation of the name given to `fields_for`/`form_with`, the index value, and the name of the attribute. You can also pass an `:index` option directly to helpers such as `text_field`, but it is usually less repetitive to specify this at the form builder level rather than on individual input controls. -As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `index: address` so +As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `index: address.id` so ```erb <%= fields_for 'person[address][primary][]', address do |address_form| %> @@ -838,10 +837,10 @@ produces exactly the same output as the previous example. Forms to External Resources --------------------------- -Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options: +Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_with` options: ```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %> +<%= form_with url: 'http://farfar.away/form', authenticity_token: 'external_token' do %> Form contents <% end %> ``` @@ -849,23 +848,7 @@ Rails' form helpers can also be used to build a form for posting data to an exte Sometimes when submitting data to an external resource, like a payment gateway, the fields that can be used in the form are limited by an external API and it may be undesirable to generate an `authenticity_token`. To not send a token, simply pass `false` to the `:authenticity_token` option: ```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: false do %> - Form contents -<% end %> -``` - -The same technique is also available for `form_for`: - -```erb -<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> - Form contents -<% end %> -``` - -Or if you don't want to render an `authenticity_token` field: - -```erb -<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> +<%= form_with url: 'http://farfar.away/form', authenticity_token: false do %> Form contents <% end %> ``` @@ -897,7 +880,7 @@ This creates an `addresses_attributes=` method on `Person` that allows you to cr The following form allows a user to create a `Person` and its associated addresses. ```html+erb -<%= form_for @person do |f| %> +<%= form_with model: @person do |f| %> Addresses: <ul> <%= f.fields_for :addresses do |addresses_form| %> @@ -948,12 +931,12 @@ The `fields_for` yields a form builder. The parameters' name will be what The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address. -If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an `id`. +If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. ### The Controller As usual you need to -[whitelist the parameters](action_controller_overview.html#strong-parameters) in +[declare the permitted parameters](action_controller_overview.html#strong-parameters) in the controller before you pass them to the model: ```ruby @@ -979,17 +962,17 @@ class Person < ApplicationRecord end ``` -If the hash of attributes for an object contains the key `_destroy` with a value -of `1` or `true` then the object will be destroyed. This form allows users to -remove addresses: +If the hash of attributes for an object contains the key `_destroy` with a value that +evaluates to `true` (eg. 1, '1', true, or 'true') then the object will be destroyed. +This form allows users to remove addresses: ```erb -<%= form_for @person do |f| %> +<%= form_with model: @person do |f| %> Addresses: <ul> <%= f.fields_for :addresses do |addresses_form| %> <li> - <%= addresses_form.check_box :_destroy%> + <%= addresses_form.check_box :_destroy %> <%= addresses_form.label :kind %> <%= addresses_form.text_field :kind %> ... @@ -999,7 +982,7 @@ remove addresses: <% end %> ``` -Don't forget to update the whitelisted params in your controller to also include +Don't forget to update the permitted params in your controller to also include the `_destroy` field: ```ruby @@ -1024,4 +1007,9 @@ As a convenience you can instead pass the symbol `:all_blank` which will create ### Adding Fields on the Fly -Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice. +Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds since the [epoch](https://en.wikipedia.org/wiki/Unix_time)) is a common choice. + +Using form_for and form_tag +--------------------------- + +Before `form_with` was introduced in Rails 5.1 its functionality used to be split between `form_tag` and `form_for`. Both are now soft-deprecated. Documentation on their usage can be found in [older versions of this guide](https://guides.rubyonrails.org/v5.2/form_helpers.html). diff --git a/guides/source/generators.md b/guides/source/generators.md index 89424a161b..88ce4be8da 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -203,8 +203,6 @@ $ rails generate scaffold User name:string create test/application_system_test_case.rb create test/system/users_test.rb invoke assets - invoke coffee - create app/assets/javascripts/users.coffee invoke scss create app/assets/stylesheets/users.scss invoke scss @@ -221,7 +219,7 @@ If we want to avoid generating the default `app/assets/stylesheets/scaffolds.scs end ``` -The next customization on the workflow will be to stop generating stylesheet, JavaScript, and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following: +The next customization on the workflow will be to stop generating stylesheet and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -229,7 +227,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false - g.javascripts false end ``` @@ -287,7 +284,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false - g.javascripts false g.helper :my_helper end ``` @@ -352,7 +348,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false - g.javascripts false end ``` @@ -387,7 +382,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :shoulda, fixture: false g.stylesheets false - g.javascripts false # Add a fallback! g.fallbacks[:shoulda] = :test_unit @@ -426,9 +420,8 @@ $ rails generate scaffold Comment body:text create test/application_system_test_case.rb create test/system/comments_test.rb invoke assets - invoke coffee - create app/assets/javascripts/comments.coffee invoke scss + create app/assets/stylesheets/scaffolds.scss ``` Fallbacks allow your generators to have a single responsibility, increasing code reuse and reducing the amount of duplication. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 88a13cdd70..e2f558d74c 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -126,7 +126,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 5.1.1", you are ready to continue. +If it says something like "Rails 5.2.1", you are ready to continue. ### Creating the Blog Application @@ -272,8 +272,6 @@ invoke helper create app/helpers/welcome_helper.rb invoke test_unit invoke assets -invoke coffee -create app/assets/javascripts/welcome.coffee invoke scss create app/assets/stylesheets/welcome.scss ``` @@ -779,10 +777,11 @@ extra fields with values that violated your application's integrity? They would be 'mass assigned' into your model and then into the database along with the good stuff - potentially breaking your application or worse. -We have to whitelist our controller parameters to prevent wrongful mass +We have to define our permitted controller parameters to prevent wrongful mass assignment. In this case, we want to both allow and require the `title` and `text` parameters for valid use of `create`. The syntax for this introduces -`require` and `permit`. The change will involve one line in the `create` action: +`require` and `permit`. The change will involve one line in the `create` +action: ```ruby @article = Article.new(params.require(:article).permit(:title, :text)) @@ -1664,7 +1663,6 @@ This creates five files and one empty directory: | app/views/comments/ | Views of the controller are stored here | | test/controllers/comments_controller_test.rb | The test for the controller | | app/helpers/comments_helper.rb | A view helper file | -| app/assets/javascripts/comments.coffee | CoffeeScript for the controller | | app/assets/stylesheets/comments.scss | Cascading style sheet for the controller | Like with any blog, our readers will create their comments directly after diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 7843df5b18..10b1a6de7e 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -77,8 +77,8 @@ There are also attribute readers and writers for the following attributes: load_path # Announce your custom translation files locale # Get and set the current locale default_locale # Get and set the default locale -available_locales # Whitelist locales available for the application -enforce_available_locales # Enforce locale whitelisting (true or false) +available_locales # Permitted locales available for the application +enforce_available_locales # Enforce locale permission (true or false) exception_handler # Use a different exception_handler backend # Use a different backend ``` @@ -116,7 +116,7 @@ NOTE: The backend lazy-loads these translations when a translation is looked up You can change the default locale as well as configure the translations load paths in `config/application.rb` as follows: ```ruby - config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] config.i18n.default_locale = :de ``` @@ -128,26 +128,29 @@ The load path must be specified before any translations are looked up. To change # Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] -# Whitelist locales available for the application +# Permitted locales available for the application I18n.available_locales = [:en, :pt] # Set default locale to something other than :en I18n.default_locale = :pt ``` +Note that appending directly to `I18n.load_paths` instead of to the application's configured i18n will _not_ override translations from external gems. + ### Managing the Locale across Requests The default locale is used for all translations unless `I18n.locale` is explicitly set. A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request. -The locale can be set in a `before_action` in the `ApplicationController`: +The locale can be set in an `around_action` in the `ApplicationController`: ```ruby -before_action :set_locale +around_action :switch_locale -def set_locale - I18n.locale = params[:locale] || I18n.default_locale +def switch_locale(&action) + locale = params[:locale] || I18n.default_locale + I18n.with_locale(locale, &action) end ``` @@ -167,10 +170,11 @@ One option you have is to set the locale from the domain name where your applica You can implement it like this in your `ApplicationController`: ```ruby -before_action :set_locale +around_action :switch_locale -def set_locale - I18n.locale = extract_locale_from_tld || I18n.default_locale +def switch_locale(&action) + locale = extract_locale_from_tld || I18n.default_locale + I18n.with_locale(locale, &action) end # Get locale from top-level domain or return +nil+ if such locale is not available @@ -210,7 +214,7 @@ This solution has aforementioned advantages, however, you may not be able or may #### Setting the Locale from URL Params -The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. +The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.with_locale(params[:locale], &action)` _around_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. This approach has almost the same set of advantages as setting the locale from the domain name: namely that it's RESTful and in accord with the rest of the World Wide Web. It does require a little bit more work to implement, though. @@ -273,8 +277,11 @@ NOTE: Have a look at various gems which simplify working with routes: [routing_f An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale preference is persisted in the database and used to set the locale for authenticated requests by that user. ```ruby -def set_locale - I18n.locale = current_user.try(:locale) || I18n.default_locale +around_action :switch_locale + +def switch_locale(&action) + locale = current_user.try(:locale) || I18n.default_locale + I18n.with_locale(locale, &action) end ``` @@ -289,10 +296,11 @@ The `Accept-Language` HTTP header indicates the preferred language for request's A trivial implementation of using an `Accept-Language` header would be: ```ruby -def set_locale +def switch_locale(&action) logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}" - I18n.locale = extract_locale_from_accept_language_header + locale = extract_locale_from_accept_language_header logger.debug "* Locale set to '#{I18n.locale}'" + I18n.with_locale(locale, &action) end private @@ -333,10 +341,12 @@ end ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base - before_action :set_locale - def set_locale - I18n.locale = params[:locale] || I18n.default_locale + around_action :switch_locale + + def switch_locale(&action) + locale = params[:locale] || I18n.default_locale + I18n.with_locale(locale, &action) end end ``` @@ -662,6 +672,26 @@ I18n.t 'activerecord.errors.messages' # => {:inclusion=>"is not included in the list", :exclusion=> ... } ``` +If you want to perform interpolation on a bulk hash of translations, you need to pass `deep_interpolation: true` as a parameter. When you have the following dictionary: + +```yaml +en: + welcome: + title: "Welcome!" + content: "Welcome to the %{app_name}" +``` + +then the nested interpolation will be ignored without the setting: + +```ruby +I18n.t 'welcome', app_name: 'book store' +# => {:title=>"Welcome!", :content=>"Welcome to the %{app_name}"} + +I18n.t 'welcome', deep_interpolation: true, app_name: 'book store' +# => {:title=>"Welcome!", :content=>"Welcome to the book store"} +``` + + #### "Lazy" Lookup Rails implements a convenient way to look up the locale inside _views_. When you have the following dictionary: @@ -1103,7 +1133,7 @@ For several reasons the Simple backend shipped with Active Support only does the That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs, by passing a backend instance to the `I18n.backend=` setter. -For example, you can replace the Simple backend with the the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. +For example, you can replace the Simple backend with the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. With the Chain backend, you could use the Active Record backend and fall back to the (default) Simple backend: diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index e8a1bd4f3d..1f42d72756 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -1,20 +1,19 @@ <!DOCTYPE html> - -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<html lang="en"> <head> -<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> -<meta name="viewport" content="width=device-width, initial-scale=1"/> - -<title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title> -<link rel="stylesheet" type="text/css" href="stylesheets/style.css" /> -<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print" /> - -<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" /> -<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shThemeRailsGuides.css" /> - -<link rel="stylesheet" type="text/css" href="stylesheets/fixes.css" /> - -<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" /> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title> + <link rel="stylesheet" type="text/css" href="stylesheets/style.css" data-turbolinks-track="reload"> + <link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print"> + <link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" data-turbolinks-track="reload"> + <link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shThemeRailsGuides.css" data-turbolinks-track="reload"> + <link rel="stylesheet" type="text/css" href="stylesheets/fixes.css" data-turbolinks-track="reload"> + <link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" /> + <script src="javascripts/syntaxhighlighter.js" data-turbolinks-track="reload"></script> + <script src="javascripts/turbolinks.js" data-turbolinks-track="reload"></script> + <script src="javascripts/guides.js" data-turbolinks-track="reload"></script> + <script src="javascripts/responsive-tables.js" data-turbolinks-track="reload"></script> </head> <body class="guide"> <% if @edge %> @@ -46,16 +45,16 @@ <a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a> <div id="guides" class="clearfix" style="display: none;"> <hr /> - <% ['L', 'R'].each do |position| %> - <dl class="<%= position %>"> - <% docs_for_menu(position).each do |section| %> - <dt><%= section['name'] %></dt> - <% finished_documents(section['documents']).each do |document| %> - <dd><a href="<%= document['url'] %>"><%= document['name'] %></a></dd> - <% end %> + <div class="guides-section-container"> + <% documents_by_section.each do |section| %> + <div class="guides-section"> + <dt><%= section['name'] %></dt> + <% finished_documents(section['documents']).each do |document| %> + <dd><a href="<%= document['url'] %>"><%= document['name'] %></a></dd> + <% end %> + </div> <% end %> - </dl> - <% end %> + </div> </div> </li> <li><a class="nav-item" href="contributing_to_ruby_on_rails.html">Contribute</a></li> @@ -95,12 +94,12 @@ </p> <p> Please contribute if you see any typos or factual errors. - To get started, you can read our <%= link_to 'documentation contributions', 'http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section. + To get started, you can read our <%= link_to 'documentation contributions', 'https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section. </p> <p> You may also find incomplete content or stuff that is not up to date. Please do add any missing documentation for master. Make sure to check - <%= link_to 'Edge Guides', 'http://edgeguides.rubyonrails.org' %> first to verify + <%= link_to 'Edge Guides', 'https://edgeguides.rubyonrails.org' %> first to verify if the issues are already fixed or not on the master branch. Check the <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %> for style and conventions. @@ -122,9 +121,5 @@ <%= render 'license' %> </div> </div> - - <script type="text/javascript" src="javascripts/syntaxhighlighter.js"></script> - <script type="text/javascript" src="javascripts/guides.js"></script> - <script type="text/javascript" src="javascripts/responsive-tables.js"></script> </body> </html> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 00da65b784..ad08e5a5a9 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -1266,7 +1266,7 @@ You can also pass in arbitrary local variables to any partial you are rendering In this case, the partial will have access to a local variable `title` with the value "Products Page". -TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. +TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. Note that it also applies for when the partial name was changed by using the `as:` option. For example, the counter variable for the code above would be `item_counter`. You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option: diff --git a/guides/source/routing.md b/guides/source/routing.md index 8c69e2600b..84de727c11 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -719,12 +719,12 @@ NOTE: There is an exception for the `format` constraint: while it's a method on ### Advanced Constraints -If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a blacklist to the `BlacklistController`. You could do: +If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a restricted list to the `RestrictedListController`. You could do: ```ruby -class BlacklistConstraint +class RestrictedListConstraint def initialize - @ips = Blacklist.retrieve_ips + @ips = RestrictedList.retrieve_ips end def matches?(request) @@ -733,8 +733,8 @@ class BlacklistConstraint end Rails.application.routes.draw do - get '*path', to: 'blacklist#index', - constraints: BlacklistConstraint.new + get '*path', to: 'restricted_list#index', + constraints: RestrictedListConstraint.new end ``` @@ -742,8 +742,8 @@ You can also specify constraints as a lambda: ```ruby Rails.application.routes.draw do - get '*path', to: 'blacklist#index', - constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) } + get '*path', to: 'restricted_list#index', + constraints: lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) } end ``` diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index f5c0ba5b2d..4b56cf6296 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -107,8 +107,8 @@ HTML Guides ----------- Before generating the guides, make sure that you have the latest version of -Bundler installed on your system. As of this writing, you must install Bundler -1.3.5 or later on your device. +Bundler installed on your system. You can find the latest Bundler version +[here](https://rubygems.org/gems/bundler). As of this writing, it's v1.17.1. To install the latest version of Bundler, run `gem install bundler`. diff --git a/guides/source/security.md b/guides/source/security.md index 9fbd252bb7..dbec3cdd2d 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -378,7 +378,7 @@ This will redirect the user to the main action if they tried to access a legacy http://www.example.com/site/legacy?param1=xy¶m2=23&host=www.attacker.com ``` -If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a whitelist approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a whitelist or a regular expression_. +If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a permitted list approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a permitted list or a regular expression_. #### Self-contained XSS @@ -394,7 +394,7 @@ NOTE: _Make sure file uploads don't overwrite important files, and process media Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers, and other programs as a less privileged Unix user. -When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master): +When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a permitted list approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a restricted list approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master): ```ruby def sanitize_filename(filename) @@ -641,19 +641,19 @@ INFO: _Injection is a class of attacks that introduce malicious code or paramete Injection is very tricky, because the same code or parameter can be malicious in one context, but totally harmless in another. A context can be a scripting, query, or programming language, the shell, or a Ruby/Rails method. The following sections will cover all important contexts where injection attacks may happen. The first section, however, covers an architectural decision in connection with Injection. -### Whitelists versus Blacklists +### Permitted lists versus Restricted lists -NOTE: _When sanitizing, protecting, or verifying something, prefer whitelists over blacklists._ +NOTE: _When sanitizing, protecting, or verifying something, prefer permitted lists over restricted lists._ -A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags, and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_: +A restricted list can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a permitted list which lists the good e-mail addresses, public actions, good HTML tags, and so on. Although sometimes it is not possible to create a permitted list (in a SPAM filter, for example), _prefer to use permitted list approaches_: * Use before_action except: [...] instead of only: [...] for security-related actions. This way you don't forget to enable security checks for newly added actions. * Allow <strong> instead of removing <script> against Cross-Site Scripting (XSS). See below for details. -* Don't try to correct user input by blacklists: +* Don't try to correct user input using restricted lists: * This will make the attack work: "<sc<script>ript>".gsub("<script>", "") * But reject malformed input -Whitelists are also a good approach against the human factor of forgetting something in the blacklist. +Permitted lists are also a good approach against the human factor of forgetting something in the restricted list. ### SQL Injection @@ -810,15 +810,15 @@ http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode _It is very important to filter malicious input, but it is also important to escape the output of the web application_. -Especially for XSS, it is important to do _whitelist input filtering instead of blacklist_. Whitelist filtering states the values allowed as opposed to the values not allowed. Blacklists are never complete. +Especially for XSS, it is important to do _permitted input filtering instead of restricted_. Permitted list filtering states the values allowed as opposed to the values not allowed. Restricted lists are never complete. -Imagine a blacklist deletes "script" from the user input. Now the attacker injects "<scrscriptipt>", and after the filter, "<script>" remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible: +Imagine a restricted list deletes "script" from the user input. Now the attacker injects "<scrscriptipt>", and after the filter, "<script>" remains. Earlier versions of Rails used a restricted list approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible: ```ruby strip_tags("some<<b>script>alert('hello')<</b>/script>") ``` -This returned "some<script>alert('hello')</script>", which makes an attack work. That's why a whitelist approach is better, using the updated Rails 2 method sanitize(): +This returned "some<script>alert('hello')</script>", which makes an attack work. That's why a permitted list approach is better, using the updated Rails 2 method sanitize(): ```ruby tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p) @@ -852,7 +852,7 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ... ``` -The worms exploit a hole in Yahoo's HTML/JavaScript filter, which usually filters all targets and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. +The worms exploit a hole in Yahoo's HTML/JavaScript filter, which usually filters all targets and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why restricted list filters are never complete and why it is hard to allow HTML/JavaScript in a web application. Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with. @@ -876,7 +876,7 @@ So the payload is in the style attribute. But there are no quotes allowed in the <div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')"> ``` -The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word "innerHTML": +The eval() function is a nightmare for restricted list input filters, as it allows the style attribute to hide the word "innerHTML": ``` alert(eval('document.body.inne' + 'rHTML')); @@ -896,7 +896,7 @@ The [moz-binding](http://www.securiteam.com/securitynews/5LP051FHPE.html) CSS pr #### Countermeasures -This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one. +This example, again, showed that a restricted list filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good permitted CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a permitted CSS filter, if you really need one. ### Textile Injection @@ -925,7 +925,7 @@ RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html #### Countermeasures -It is recommended to _use RedCloth in combination with a whitelist input filter_, as described in the countermeasures against XSS section. +It is recommended to _use RedCloth in combination with a permitted input filter_, as described in the countermeasures against XSS section. ### Ajax Injection @@ -1235,6 +1235,11 @@ version: Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank ``` +Dependency Management and CVEs +------------------------------ + +We don’t bump dependencies just to encourage use of new versions, including for security issues. This is because application owners need to manually update their gems regardless of our efforts. Use `bundle update --conservative gem_name` to safely update vulnerable dependencies. + Additional Resources -------------------- diff --git a/guides/source/testing.md b/guides/source/testing.md index 01cda8e6e4..8c21ccfba6 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1028,8 +1028,8 @@ You should test for things such as: * was the web request successful? * was the user redirected to the right page? * was the user successfully authenticated? -* was the correct object stored in the response template? * was the appropriate message displayed to the user in the view? +* was the correct information displayed in the response? The easiest way to see functional tests in action is to generate a controller using the scaffold generator: @@ -1113,11 +1113,10 @@ end Now you can try running all the tests and they should pass. -NOTE: If you followed the steps in the Basic Authentication section, you'll need to add the following to the `setup` block to get all the tests passing: +NOTE: If you followed the steps in the Basic Authentication section, you'll need to add authorization to every request header to get all the tests passing: ```ruby -request.headers['Authorization'] = ActionController::HttpAuthentication::Basic. - encode_credentials('dhh', 'secret') +post articles_url, params: { article: { body: 'Rails is awesome!', title: 'Hello Rails' } }, headers: { Authorization: ActionController::HttpAuthentication::Basic.encode_credentials('dhh', 'secret') } ``` ### Available Request Types for Functional Tests @@ -1563,7 +1562,7 @@ class UserMailerTest < ActionMailer::TestCase end ``` -In the test we send the email and store the returned object in the `email` +In the test we create the email and store the returned object in the `email` variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect. The helper `read_fixture` is used to read in the content from this file. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 319bc09be3..a0553c1ccc 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -41,7 +41,7 @@ Rails generally stays close to the latest released Ruby version when it's releas * Rails 3.2.x is the last branch to support Ruby 1.8.7. * Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible. -TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing. +TIP: Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing. ### The Update Task @@ -66,9 +66,18 @@ Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] Don't forget to review the difference, to see if there were any unexpected changes. +### Configure Framework Defaults + +The new Rails version might have different configuration defaults than the previous version. However, after following the steps described above, your application would still run with configuration defaults from the *previous* Rails version. That's because the value for `config.load_defaults` in `config/application.rb` has not been changed yet. + +To allow you to upgrade to new defaults one by one, the update task has created a file `config/initializers/new_framework_defaults.rb`. Once your application is ready to run with new defaults, you can remove this file and flip the `config.load_defaults` value. + + Upgrading from Rails 5.2 to Rails 6.0 ------------------------------------- +For more information on changes made to Rails 6.0 please see the [release notes](6_0_release_notes.html). + ### Force SSL The `force_ssl` method on controllers has been deprecated and will be removed in @@ -76,6 +85,17 @@ Rails 6.1. You are encouraged to enable `config.force_ssl` to enforce HTTPS connections throughout your application. If you need to exempt certain endpoints from redirection, you can use `config.ssl_options` to configure that behavior. +### Purpose in signed or encrypted cookie is now embedded in the cookies values + +To improve security, Rails now embeds the purpose information in encrypted or signed cookies value. +Rails can now thwart attacks that attempt to copy signed/encrypted value +of a cookie and use it as the value of another cookie. + +This new embed information make those cookies incompatible with versions of Rails older than 6.0. + +If you require your cookies to be read by 5.2 and older, or you are still validating your 6.0 deploy and want +to allow you to rollback set +`Rails.application.config.action_dispatch.use_cookies_with_metadata` to `false`. Upgrading from Rails 5.1 to Rails 5.2 ------------------------------------- diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 36f5039883..c36b3faa6c 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -494,10 +494,6 @@ replace the entire `<body>` of the page with the `<body>` of the response. It will then use PushState to change the URL to the correct one, preserving refresh semantics and giving you pretty URLs. -The only thing you have to do to enable Turbolinks is have it in your `Gemfile`, -and put `//= require turbolinks` in your JavaScript manifest, which is usually -`app/assets/javascripts/application.js`. - If you want to disable Turbolinks for certain links, add a `data-turbolinks="false"` attribute to the tag: diff --git a/package.json b/package.json new file mode 100644 index 0000000000..88029de141 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "workspaces": [ + "actioncable", + "activestorage", + "actionview", + "tmp/templates/app_template", + "railties/test/fixtures/tmp/bukkits/**/test/dummy", + "railties/test/fixtures/tmp/bukkits/**/spec/dummy" + ], + "dependencies": { + "webpack": "^4.17.1" + } +} diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 81cb5a6c9f..65dccbb3be 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,10 +1,101 @@ +* Add JSON support to rails properties route (`/rails/info/properties`). + + Now, `Rails::Info` properties may be accessed in JSON format at `/rails/info/properties.json`. + + *Yoshiyuki Hirano* + +* Use Ids instead of memory addresses when displaying references in scaffold views. + + Fixes #29200. + + *Rasesh Patel* + +* Adds support for multiple databases to `rails db:migrate:status`. + Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`). + + *Gannon McGibbon* + +* Use Webpacker by default to manage app-level JavaScript through the new app/javascript directory. + Sprockets is now solely in charge, by default, of compiling CSS and other static assets. + Action Cable channel generators will create ES6 stubs rather than use CoffeeScript. + Active Storage, Action Cable, Turbolinks, and Rails-UJS are loaded by a new application.js pack. + Generators no longer generate JavaScript stubs. + + *DHH*, *Lachlan Sylvester* + +* Add `database` (aliased as `db`) option to model generator to allow + setting the database. This is useful for applications that use + multiple databases and put migrations per database in their own directories. + + ``` + bin/rails g model Room capacity:integer --database=kingston + invoke active_record + create db/kingston_migrate/20180830151055_create_rooms.rb + ``` + + Because rails scaffolding uses the model generator, you can + also specify a database with the scaffold generator. + + *Gannon McGibbon* + +* Raise an error when "recyclable cache keys" are being used by a cache store + that does not explicitly support it. Custom cache keys that do support this feature + can bypass this error by implementing the `supports_cache_versioning?` method on their + class and returning a truthy value. + + *Richard Schneeman* + +* Support environment specific credentials file. + + For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by + `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key. + Edit given environment credentials file by command `rails credentials:edit --environment production`. + Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`. + + *Wojciech Wnętrzak* + +* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment. + + *Michael C. Nelson* + +* Emit warning for unknown inflection rule when generating model. + + *Yoshiyuki Kinjo* + +* Add `database` (aliased as `db`) option to migration generator. + + If you're using multiple databases and have a folder for each database + for migrations (ex db/migrate and db/new_db_migrate) you can now pass the + `--database` option to the generator to make sure the the migration + is inserted into the correct folder. + + ``` + rails g migration CreateHouses --database=kingston + invoke active_record + create db/kingston_migrate/20180830151055_create_houses.rb + ``` + + *Eileen M. Uchitelle* + +* Deprecate `rake routes` in favor of `rails routes`. + + *Yuji Yaginuma* + +* Deprecate `rake initializers` in favor of `rails initializers`. + + *Annie-Claude Côté* + +* Deprecate `rake dev:cache` in favor of `rails dev:cache`. + + *Annie-Claude Côté* + * Deprecate `rails notes` subcommands in favor of passing an `annotations` argument to `rails notes`. The following subcommands are replaced by passing `--annotations` or `-a` to `rails notes`: - - `rails notes:custom ANNOTATION=custom` is deprecated in favor of using `rails notes -a custom`. - - `rails notes:optimize` is deprecated in favor of using `rails notes -a OPTIMIZE`. - - `rails notes:todo` is deprecated in favor of using`rails notes -a TODO`. - - `rails notes:fixme` is deprecated in favor of using `rails notes -a FIXME`. + - `rails notes:custom ANNOTATION=custom` is deprecated in favor of using `rails notes -a custom`. + - `rails notes:optimize` is deprecated in favor of using `rails notes -a OPTIMIZE`. + - `rails notes:todo` is deprecated in favor of using`rails notes -a TODO`. + - `rails notes:fixme` is deprecated in favor of using `rails notes -a FIXME`. *Annie-Claude Côté* @@ -17,13 +108,13 @@ *Annie-Claude Côté* -* Don't generate unused files in `app:update` task +* Don't generate unused files in `app:update` task. - Skip the assets' initializer when sprockets isn't loaded. + Skip the assets' initializer when sprockets isn't loaded. - Skip `config/spring.rb` when spring isn't loaded. + Skip `config/spring.rb` when spring isn't loaded. - Skip yarn's contents when yarn integration isn't used. + Skip yarn's contents when yarn integration isn't used. *Tsukuru Tanimichi* diff --git a/railties/Rakefile b/railties/Rakefile index e1be0ceb40..445f6217b3 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -28,6 +28,9 @@ namespace :test do require "bundler/setup" unless defined?(Bundler) require "active_support" + # Only generate the template app once. + require_relative "test/isolation/abstract_unit" + failing_files = [] dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",") diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb index 3405560b74..126d4d0438 100644 --- a/railties/lib/rails/api/generator.rb +++ b/railties/lib/rails/api/generator.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "sdoc" +require "active_support/core_ext/array/extract" class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc: RDoc::RDoc.add_generator self @@ -11,7 +12,7 @@ class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc: # since they aren't nested under a definition of the `ActiveStorage` module. if visited.empty? classes = classes.reject { |klass| active_storage?(klass) } - core_exts, classes = classes.partition { |klass| core_extension?(klass) } + core_exts = classes.extract! { |klass| core_extension?(klass) } super.unshift([ "Core extensions", "", "", build_core_ext_subtree(core_exts, visited) ]) else diff --git a/railties/lib/rails/app_updater.rb b/railties/lib/rails/app_updater.rb index a243968a39..19d136e041 100644 --- a/railties/lib/rails/app_updater.rb +++ b/railties/lib/rails/app_updater.rb @@ -21,7 +21,7 @@ module Rails private def generator_options options = { api: !!Rails.application.config.api_only, update: true } - options[:skip_yarn] = !File.exist?(Rails.root.join("bin", "yarn")) + options[:skip_javascript] = !File.exist?(Rails.root.join("bin", "yarn")) options[:skip_active_record] = !defined?(ActiveRecord::Railtie) options[:skip_active_storage] = !defined?(ActiveStorage::Engine) || !defined?(ActiveRecord::Railtie) options[:skip_action_mailer] = !defined?(ActionMailer::Railtie) diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 31d0d65a30..656786246d 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -232,7 +232,10 @@ module Rails if yaml.exist? require "erb" - (YAML.load(ERB.new(yaml.read).result) || {})[env] || {} + require "active_support/ordered_options" + + config = (YAML.load(ERB.new(yaml.read).result) || {})[env] || {} + ActiveSupport::InheritableOptions.new(config.deep_symbolize_keys) else raise "Could not load configuration. No such file - #{yaml}" end @@ -267,6 +270,7 @@ module Rails "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest, "action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations, + "action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata, "action_dispatch.content_security_policy" => config.content_security_policy, "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only, "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator @@ -434,8 +438,12 @@ module Rails # Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with # the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading # +config/master.key+. + # If specific credentials file exists for current environment, it takes precedence, thus for +production+ + # environment look first for +config/credentials/production.yml.enc+ with master key taken + # from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading +config/credentials/production.key+. + # Default behavior can be overwritten by setting +config.credentials.content_path+ and +config.credentials.key_path+. def credentials - @credentials ||= encrypted("config/credentials.yml.enc") + @credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path) end # Shorthand to decrypt any encrypted configurations or files. diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 9c54cc1f37..d6f8c4f47c 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -17,7 +17,7 @@ module Rails :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, :read_encrypted_secrets, :log_level, :content_security_policy_report_only, - :content_security_policy_nonce_generator, :require_master_key + :content_security_policy_nonce_generator, :require_master_key, :credentials attr_reader :encoding, :api_only, :loaded_config_version @@ -60,6 +60,9 @@ module Rails @content_security_policy_nonce_generator = nil @require_master_key = false @loaded_config_version = nil + @credentials = ActiveSupport::OrderedOptions.new + @credentials.content_path = default_credentials_content_path + @credentials.key_path = default_credentials_key_path end def load_defaults(target_version) @@ -120,6 +123,10 @@ module Rails if respond_to?(:action_view) action_view.default_enforce_utf8 = false end + + if respond_to?(:action_dispatch) + action_dispatch.use_cookies_with_metadata = true + end else raise "Unknown version #{target_version.to_s.inspect}" end @@ -164,18 +171,6 @@ module Rails end end - # Loads the database YAML without evaluating ERB. People seem to - # write ERB that makes the database configuration depend on - # Rails configuration. But we want Rails configuration (specifically - # `rake` and `rails` tasks) to be generated based on information in - # the database yaml, so we need a method that loads the database - # yaml *without* the context of the Rails application. - def load_database_yaml # :nodoc: - path = paths["config/database"].existent.first - return {} unless path - YAML.load_file(path.to_s) - end - # Loads and returns the entire raw configuration of database from # values stored in <tt>config/database.yml</tt>. def database_configuration @@ -207,7 +202,7 @@ module Rails "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ "Error: #{e.message}" rescue => e - raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace + raise e, "Cannot load database configuration:\n#{e.message}", e.backtrace end def colorize_logging @@ -281,6 +276,27 @@ module Rails true end end + + private + def credentials_available_for_current_env? + File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc") + end + + def default_credentials_content_path + if credentials_available_for_current_env? + File.join(root, "config", "credentials", "#{Rails.env}.yml.enc") + else + File.join(root, "config", "credentials.yml.enc") + end + end + + def default_credentials_key_path + if credentials_available_for_current_env? + File.join(root, "config", "credentials", "#{Rails.env}.key") + else + File.join(root, "config", "master.key") + end + end end end end diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb index 0e78959966..7c2eb1dc42 100644 --- a/railties/lib/rails/backtrace_cleaner.rb +++ b/railties/lib/rails/backtrace_cleaner.rb @@ -6,29 +6,17 @@ module Rails class BacktraceCleaner < ActiveSupport::BacktraceCleaner APP_DIRS_PATTERN = /^\/?(app|config|lib|test|\(\w*\))/ RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/ - EMPTY_STRING = "".freeze - SLASH = "/".freeze - DOT_SLASH = "./".freeze + EMPTY_STRING = "" + SLASH = "/" + DOT_SLASH = "./" def initialize super - @root = "#{Rails.root}/".freeze + @root = "#{Rails.root}/" add_filter { |line| line.sub(@root, EMPTY_STRING) } add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) } add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests - - add_gem_filters add_silencer { |line| !APP_DIRS_PATTERN.match?(line) } end - - private - def add_gem_filters - gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) } - return if gems_paths.empty? - - gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)} - gems_result = '\2 (\3) \4'.freeze - add_filter { |line| line.sub(gems_regexp, gems_result) } - end end end diff --git a/railties/lib/rails/command/spellchecker.rb b/railties/lib/rails/command/spellchecker.rb index 04485097fa..085d5b16df 100644 --- a/railties/lib/rails/command/spellchecker.rb +++ b/railties/lib/rails/command/spellchecker.rb @@ -24,8 +24,8 @@ module Rails n = s.length m = t.length - return m if (0 == n) - return n if (0 == m) + return m if 0 == n + return n if 0 == m d = (0..m).to_a x = nil diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE index ea429f58d8..6b33d1ab74 100644 --- a/railties/lib/rails/commands/credentials/USAGE +++ b/railties/lib/rails/commands/credentials/USAGE @@ -38,3 +38,12 @@ the encrypted credentials. When the temporary file is next saved the contents are encrypted and written to `config/credentials.yml.enc` while the file itself is destroyed to prevent credentials from leaking. + +=== Environment Specific Credentials + +It is possible to have credentials for each environment. If the file for current environment exists it will take +precedence over `config/credentials.yml.enc`, thus for `production` environment first look for +`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]` +or stored in `config/credentials/production.key`. +To edit given file use command `rails credentials:edit --environment production` +Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`. diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb index 65c5218fc8..4b30d208e0 100644 --- a/railties/lib/rails/commands/credentials/credentials_command.rb +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -8,6 +8,9 @@ module Rails class CredentialsCommand < Rails::Command::Base # :nodoc: include Helpers::Editor + class_option :environment, aliases: "-e", type: :string, + desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" + no_commands do def help say "Usage:\n #{self.class.banner}" @@ -20,58 +23,74 @@ module Rails require_application_and_environment! ensure_editor_available(command: "bin/rails credentials:edit") || (return) - ensure_master_key_has_been_added if Rails.application.credentials.key.nil? - ensure_credentials_have_been_added + + encrypted = Rails.application.encrypted(content_path, key_path: key_path) + + ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil? + ensure_encrypted_file_has_been_added(content_path, key_path) catch_editing_exceptions do - change_credentials_in_system_editor + change_encrypted_file_in_system_editor(content_path, key_path) end - say "New credentials encrypted and saved." + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?" end def show require_application_and_environment! - say Rails.application.credentials.read.presence || missing_credentials_message + encrypted = Rails.application.encrypted(content_path, key_path: key_path) + + say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path) end private - def ensure_master_key_has_been_added - master_key_generator.add_master_key_file - master_key_generator.ignore_master_key_file + def content_path + options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" + end + + def key_path + options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" + end + + + def ensure_encryption_key_has_been_added(key_path) + encryption_key_file_generator.add_key_file(key_path) + encryption_key_file_generator.ignore_key_file(key_path) end - def ensure_credentials_have_been_added - credentials_generator.add_credentials_file_silently + def ensure_encrypted_file_has_been_added(file_path, key_path) + encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) end - def change_credentials_in_system_editor - Rails.application.credentials.change do |tmp_path| + def change_encrypted_file_in_system_editor(file_path, key_path) + Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path| system("#{ENV["EDITOR"]} #{tmp_path}") end end - def master_key_generator + def encryption_key_file_generator require "rails/generators" - require "rails/generators/rails/master_key/master_key_generator" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" - Rails::Generators::MasterKeyGenerator.new + Rails::Generators::EncryptionKeyFileGenerator.new end - def credentials_generator + def encrypted_file_generator require "rails/generators" - require "rails/generators/rails/credentials/credentials_generator" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" - Rails::Generators::CredentialsGenerator.new + Rails::Generators::EncryptedFileGenerator.new end - def missing_credentials_message - if Rails.application.credentials.key.nil? - "Missing master key to decrypt credentials. See `rails credentials:help`" + def missing_encrypted_message(key:, key_path:, file_path:) + if key.nil? + "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" else - "No credentials have been added yet. Use `rails credentials:edit` to change that." + "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that." end end end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb index 806b7de6d6..0fac7d34a0 100644 --- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -75,7 +75,7 @@ module Rails args += ["-P", "#{config['password']}"] if config["password"] if config["host"] - host_arg = "#{config['host']}".dup + host_arg = +"#{config['host']}" host_arg << ":#{config['port']}" if config["port"] args += ["-S", host_arg] end diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb new file mode 100644 index 0000000000..a3f02f3172 --- /dev/null +++ b/railties/lib/rails/commands/dev/dev_command.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "rails/dev_caching" + +module Rails + module Command + class DevCommand < Base # :nodoc: + def help + say "rails dev:cache # Toggle development mode caching on/off." + end + + def cache + Rails::DevCaching.enable_by_file + end + end + end +end diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb index 8e5b4d68d3..9df34e9b79 100644 --- a/railties/lib/rails/commands/help/help_command.rb +++ b/railties/lib/rails/commands/help/help_command.rb @@ -6,7 +6,7 @@ module Rails hide_command! def help(*) - puts self.class.desc + say self.class.desc Rails::Command.print_commands end diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb new file mode 100644 index 0000000000..33596177af --- /dev/null +++ b/railties/lib/rails/commands/initializers/initializers_command.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Rails + module Command + class InitializersCommand < Base # :nodoc: + desc "initializers", "Print out all defined initializers in the order they are invoked by Rails." + def perform + require_application_and_environment! + + Rails.application.initializers.tsort_each do |initializer| + say "#{initializer.context_class}.#{initializer.name}" + end + end + end + end +end diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb index d73d64d899..a4f2081510 100644 --- a/railties/lib/rails/commands/new/new_command.rb +++ b/railties/lib/rails/commands/new/new_command.rb @@ -10,8 +10,8 @@ module Rails end def perform(*) - puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" - puts "Type 'rails' for help." + say "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" + say "Type 'rails' for help." exit 1 end end diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb index 2b192abf9b..96187aa952 100644 --- a/railties/lib/rails/commands/plugin/plugin_command.rb +++ b/railties/lib/rails/commands/plugin/plugin_command.rb @@ -26,7 +26,7 @@ module Rails if File.exist?(railsrc) extra_args = File.read(railsrc).split(/\n+/).flat_map(&:split) - puts "Using #{extra_args.join(" ")} from #{railsrc}" + say "Using #{extra_args.join(" ")} from #{railsrc}" plugin_args.insert(1, *extra_args) end end diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb index 30fbf04982..cb693bcf34 100644 --- a/railties/lib/rails/commands/runner/runner_command.rb +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -10,7 +10,7 @@ module Rails no_commands do def help super - puts self.class.desc + say self.class.desc end end @@ -39,11 +39,11 @@ module Rails else begin eval(code_or_file, TOPLEVEL_BINDING, __FILE__, __LINE__) - rescue SyntaxError, NameError => error - $stderr.puts "Please specify a valid ruby command or the path of a script to run." - $stderr.puts "Run '#{self.class.executable} -h' for help." - $stderr.puts - $stderr.puts error + rescue SyntaxError, NameError => e + error "Please specify a valid ruby command or the path of a script to run." + error "Run '#{self.class.executable} -h' for help." + error "" + error e exit 1 end end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 2c5440d9ec..70789e0303 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -109,7 +109,7 @@ module Rails RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn) DEFAULT_PORT = 3000 - DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze + DEFAULT_PID_PATH = "tmp/pids/server.pid" argument :using, optional: true @@ -286,7 +286,7 @@ module Rails original_options.concat [ "-u", using ] else # Use positional internally to get around Thor's immutable options. - # TODO: Replace `using` occurences with `options[:using]` after deprecation removal. + # TODO: Replace `using` occurrences with `options[:using]` after deprecation removal. @using = options[:using] end end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index ed672ae48e..5e8cebc50a 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -33,8 +33,6 @@ module Rails rails: { actions: "-a", orm: "-o", - javascripts: "-j", - javascript_engine: "-je", resource_controller: "-c", scaffold_controller: "-c", stylesheets: "-y", @@ -56,8 +54,6 @@ module Rails force_plural: false, helper: true, integration_tool: nil, - javascripts: true, - javascript_engine: :js, orm: false, resource_controller: :controller, resource_route: true, diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index ae395708cb..78d2471890 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -7,7 +7,7 @@ module Rails module Actions def initialize(*) # :nodoc: super - @in_group = nil + @indentation = 0 @after_bundle_callbacks = [] end @@ -36,13 +36,11 @@ module Rails log :gemfile, message - options.each do |option, value| - parts << "#{option}: #{quote(value)}" - end + parts << quote(options) unless options.empty? in_root do str = "gem #{parts.join(", ")}" - str = " " + str if @in_group + str = indentation + str str = "\n" + str append_file "Gemfile", str, verbose: false end @@ -54,17 +52,29 @@ module Rails # gem "rspec-rails" # end def gem_group(*names, &block) - name = names.map(&:inspect).join(", ") - log :gemfile, "group #{name}" + options = names.extract_options! + str = names.map(&:inspect) + str << quote(options) unless options.empty? + str = str.join(", ") + log :gemfile, "group #{str}" in_root do - append_file "Gemfile", "\ngroup #{name} do", force: true + append_file "Gemfile", "\ngroup #{str} do", force: true + with_indentation(&block) + append_file "Gemfile", "\nend\n", force: true + end + end - @in_group = true - instance_eval(&block) - @in_group = false + def github(repo, options = {}, &block) + str = [quote(repo)] + str << quote(options) unless options.empty? + str = str.join(", ") + log :github, "github #{str}" - append_file "Gemfile", "\nend\n", force: true + in_root do + append_file "Gemfile", "\n#{indentation}github #{str} do", force: true + with_indentation(&block) + append_file "Gemfile", "\n#{indentation}end", force: true end end @@ -83,9 +93,7 @@ module Rails in_root do if block append_file "Gemfile", "\nsource #{quote(source)} do", force: true - @in_group = true - instance_eval(&block) - @in_group = false + with_indentation(&block) append_file "Gemfile", "\nend\n", force: true else prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false @@ -315,6 +323,11 @@ module Rails # Surround string with single quotes if there is no quotes. # Otherwise fall back to double quotes def quote(value) # :doc: + if value.respond_to? :each_pair + return value.map do |k, v| + "#{k}: #{quote(v)}" + end.join(", ") + end return value.inspect unless value.is_a? String if value.include?("'") @@ -334,6 +347,19 @@ module Rails "#{value.strip.indent(amount)}\n" end end + + # Indent the +Gemfile+ to the depth of @indentation + def indentation # :doc: + " " * @indentation + end + + # Manage +Gemfile+ indentation for a DSL action block + def with_indentation(&block) # :doc: + @indentation += 1 + instance_eval(&block) + ensure + @indentation -= 1 + end end end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 985e9ab263..4dc4d27a46 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -31,9 +31,6 @@ module Rails class_option :database, type: :string, aliases: "-d", default: "sqlite3", desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - class_option :skip_yarn, type: :boolean, default: false, - desc: "Don't use Yarn for managing JavaScript dependencies" - class_option :skip_gemfile, type: :boolean, default: false, desc: "Don't create a Gemfile" @@ -68,10 +65,7 @@ module Rails class_option :skip_listen, type: :boolean, default: false, desc: "Don't generate configuration that depends on the listen gem" - class_option :skip_coffee, type: :boolean, default: false, - desc: "Don't use CoffeeScript" - - class_option :skip_javascript, type: :boolean, aliases: "-J", default: false, + class_option :skip_javascript, type: :boolean, aliases: "-J", default: name == "plugin", desc: "Skip JavaScript files" class_option :skip_turbolinks, type: :boolean, default: false, @@ -299,7 +293,7 @@ module Rails def gem_for_database # %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) case options[:database] - when "mysql" then ["mysql2", [">= 0.4.4", "< 0.6.0"]] + when "mysql" then ["mysql2", [">= 0.4.4"]] when "postgresql" then ["pg", [">= 0.18", "< 2.0"]] when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] when "frontbase" then ["ruby-frontbase", nil] @@ -327,24 +321,17 @@ module Rails def assets_gemfile_entry return [] if options[:skip_sprockets] - gems = [] - gems << GemfileEntry.version("sass-rails", "~> 5.0", - "Use SCSS for stylesheets") - - if !options[:skip_javascript] - gems << GemfileEntry.version("uglifier", - ">= 1.3.0", - "Use Uglifier as compressor for JavaScript assets") - end - - gems + GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets") end def webpacker_gemfile_entry - return [] unless options[:webpack] + return [] if options[:skip_javascript] - comment = "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" - GemfileEntry.new "webpacker", nil, comment + if options.dev? || options.edge? + GemfileEntry.github "webpacker", "rails/webpacker", nil, "Use development version of Webpacker" + else + GemfileEntry.new "webpacker", nil, "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" + end end def jbuilder_gemfile_entry @@ -352,34 +339,12 @@ module Rails GemfileEntry.new "jbuilder", "~> 2.5", comment, {}, options[:api] end - def coffee_gemfile_entry - GemfileEntry.version "coffee-rails", "~> 4.2", "Use CoffeeScript for .coffee assets and views" - end - def javascript_gemfile_entry - if options[:skip_javascript] || options[:skip_sprockets] + if options[:skip_javascript] || options[:skip_turbolinks] [] else - gems = [javascript_runtime_gemfile_entry] - gems << coffee_gemfile_entry unless options[:skip_coffee] - - unless options[:skip_turbolinks] - gems << GemfileEntry.version("turbolinks", "~> 5", - "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks") - end - - gems - end - end - - def javascript_runtime_gemfile_entry - comment = "See https://github.com/rails/execjs#readme for more supported runtimes" - if defined?(JRUBY_VERSION) - GemfileEntry.version "therubyrhino", nil, comment - elsif RUBY_PLATFORM.match?(/mingw|mswin/) - GemfileEntry.version "duktape", nil, comment - else - GemfileEntry.new "mini_racer", nil, comment, { platforms: :ruby }, true + [ GemfileEntry.version("turbolinks", "~> 5", + "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks") ] end end @@ -431,6 +396,10 @@ module Rails !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end + def webpack_install? + !(options[:skip_javascript] || options[:skip_webpack_install]) + end + def depends_on_system_test? !(options[:skip_system_test] || options[:skip_test] || options[:api]) end @@ -452,9 +421,15 @@ module Rails end def run_webpack - if !(webpack = options[:webpack]).nil? + if webpack_install? rails_command "webpacker:install" - rails_command "webpacker:install:#{webpack}" unless webpack == "webpack" + rails_command "webpacker:install:#{options[:webpack]}" if options[:webpack] && options[:webpack] != "webpack" + end + end + + def generate_bundler_binstub + if bundle_install? + bundle_command("binstubs bundler") end end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt index e1ede7c713..2cf4e5c9d0 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt @@ -16,7 +16,7 @@ <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <tr> <% attributes.reject(&:password_digest?).each do |attribute| -%> - <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> + <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td> <% end -%> <td><%%= link_to 'Show', <%= model_resource_name %> %></td> <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt index 5e634153be..7deba07926 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt @@ -3,7 +3,7 @@ <% attributes.reject(&:password_digest?).each do |attribute| -%> <p> <strong><%= attribute.human_name %>:</strong> - <%%= @<%= singular_table_name %>.<%= attribute.name %> %> + <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %> </p> <% end -%> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index f7fd30a5fb..3f20f5a718 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -153,7 +153,7 @@ module Rails end def inject_options - "".dup.tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } } + (+"").tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } } end def inject_index_options diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb deleted file mode 100644 index 9d32c666dc..0000000000 --- a/railties/lib/rails/generators/js/assets/assets_generator.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators/named_base" - -module Js # :nodoc: - module Generators # :nodoc: - class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("templates", __dir__) - - def copy_javascript - copy_file "javascript.js", File.join("app/assets/javascripts", class_path, "#{file_name}.js") - end - end - end -end diff --git a/railties/lib/rails/generators/js/assets/templates/javascript.js b/railties/lib/rails/generators/js/assets/templates/javascript.js deleted file mode 100644 index dee720facd..0000000000 --- a/railties/lib/rails/generators/js/assets/templates/javascript.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb index 50078404b3..3676432d5c 100644 --- a/railties/lib/rails/generators/model_helpers.rb +++ b/railties/lib/rails/generators/model_helpers.rb @@ -7,6 +7,10 @@ module Rails module ModelHelpers # :nodoc: PLURAL_MODEL_NAME_WARN_MESSAGE = "[WARNING] The model name '%s' was recognized as a plural, using the singular '%s' instead. " \ "Override with --force-plural or setup custom inflection rules for this noun before running the generator." + IRREGULAR_MODEL_NAME_WARN_MESSAGE = <<~WARNING + [WARNING] Rails cannot recover singular form from its plural form '%s'. + Please setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb. + WARNING mattr_accessor :skip_warn def self.included(base) #:nodoc: @@ -19,11 +23,14 @@ module Rails singular = name.singularize unless ModelHelpers.skip_warn say PLURAL_MODEL_NAME_WARN_MESSAGE % [name, singular] - ModelHelpers.skip_warn = true end name.replace singular assign_names!(name) end + if name.singularize != name.pluralize.singularize && ! ModelHelpers.skip_warn + say IRREGULAR_MODEL_NAME_WARN_MESSAGE % [name.pluralize] + end + ModelHelpers.skip_warn = true end end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 83c5c9f297..f56f79b8d4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -80,7 +80,6 @@ module Rails directory "app" keep_file "app/assets/images" - empty_directory_with_keep_file "app/assets/javascripts/channels" unless options[:skip_action_cable] keep_file "app/controllers/concerns" keep_file "app/models/concerns" @@ -96,7 +95,7 @@ module Rails def bin_when_updating bin - if options[:skip_yarn] + if options[:skip_javascript] remove_file "bin/yarn" end end @@ -260,7 +259,10 @@ module Rails desc: "Don't run bundle install" class_option :webpack, type: :string, default: nil, - desc: "Preconfigure for app-like JavaScript with Webpack (options: #{WEBPACKS.join('/')})" + desc: "Preconfigure Webpack with a particular framework (options: #{WEBPACKS.join('/')})" + + class_option :skip_webpack_install, type: :boolean, default: false, + desc: "Don't run Webpack install" def initialize(*args) super @@ -272,7 +274,7 @@ module Rails # Force sprockets and yarn to be skipped when generating API only apps. # Can't modify options hash as it's frozen by default. if options[:api] - self.options = options.merge(skip_sprockets: true, skip_javascript: true, skip_yarn: true).freeze + self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze end end @@ -287,7 +289,7 @@ module Rails build(:gitignore) unless options[:skip_git] build(:gemfile) unless options[:skip_gemfile] build(:version_control) - build(:package_json) unless options[:skip_yarn] + build(:package_json) unless options[:skip_javascript] end def create_app_files @@ -409,7 +411,7 @@ module Rails def delete_js_folder_skipping_javascript if options[:skip_javascript] - remove_dir "app/assets/javascripts" + remove_dir "app/javascript" end end @@ -436,7 +438,7 @@ module Rails def delete_action_cable_files_skipping_action_cable if options[:skip_action_cable] - remove_file "app/assets/javascripts/cable.js" + remove_dir "app/javascript/channels" remove_dir "app/channels" end end @@ -460,8 +462,8 @@ module Rails end end - def delete_bin_yarn_if_skip_yarn_option - remove_file "bin/yarn" if options[:skip_yarn] + def delete_bin_yarn + remove_file "bin/yarn" if options[:skip_javascript] end def finish_template @@ -469,7 +471,8 @@ module Rails end public_task :apply_rails_template, :run_bundle - public_task :run_webpack, :generate_spring_binstubs + public_task :generate_bundler_binstub, :generate_spring_binstubs + public_task :run_webpack def run_after_bundle_callbacks @after_bundle_callbacks.each(&:call) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 1567333023..fb264935bd 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -45,6 +45,7 @@ group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end +<% end -%> group :development do <%- unless options.api? -%> # Access an interactive console on exception pages or by calling 'console' anywhere in the code. @@ -75,7 +76,6 @@ group :test do gem 'chromedriver-helper' end <%- end -%> -<% end -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt index 70b579d10e..591819335f 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt @@ -1,5 +1,2 @@ //= link_tree ../images -<% unless options.skip_javascript -%> -//= link_directory ../javascripts .js -<% end -%> //= link_directory ../stylesheets .css diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt deleted file mode 100644 index 5183bcd256..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt +++ /dev/null @@ -1,22 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's -// vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. JavaScript code in this file should be added after the last require_* statement. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -<% unless options[:skip_javascript] -%> -//= require rails-ujs -<% unless skip_active_storage? -%> -//= require activestorage -<% end -%> -<% unless options[:skip_turbolinks] -%> -//= require turbolinks -<% end -%> -<% end -%> -//= require_tree . diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js index 739aa5f022..76ca3d0f2f 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js @@ -1,13 +1,6 @@ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. -// -//= require action_cable -//= require_self -//= require_tree ./channels -(function() { - this.App || (this.App = {}); +import ActionCable from "actioncable" - App.cable = ActionCable.createConsumer(); - -}).call(this); +export default ActionCable.createConsumer() diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js new file mode 100644 index 0000000000..0cfcf74919 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/) +channels.keys().forEach(channels) diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt new file mode 100644 index 0000000000..4d7a145cd6 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt @@ -0,0 +1,21 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import Rails from "rails-ujs" +Rails.start() +<%- unless options[:skip_turbolinks] -%> + +import Turbolinks from "turbolinks" +Turbolinks.start() +<%- end -%> +<%- unless skip_active_storage? -%> + +import * as ActiveStorage from "activestorage" +ActiveStorage.start() +<%- end -%> +<%- unless options[:skip_action_cable] -%> + +import "channels" +<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt index a009ace51c..d394c3d106 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt @@ -1,2 +1,7 @@ class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError end diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index ef715f1368..9a7267c783 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -9,11 +9,11 @@ <%%= stylesheet_link_tag 'application', media: 'all' %> <%- else -%> <%- unless options[:skip_turbolinks] -%> - <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> - <%%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> + <%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <%- else -%> - <%%= stylesheet_link_tag 'application', media: 'all' %> - <%%= javascript_include_tag 'application' %> + <%%= stylesheet_link_tag 'application', media: 'all' %> + <%%= javascript_pack_tag 'application' %> <%- end -%> <%- end -%> </head> diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt b/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt deleted file mode 100644 index a84f0afe47..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt +++ /dev/null @@ -1,2 +0,0 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt index 233b5a1d95..a18e03e7db 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -1,5 +1,4 @@ require 'fileutils' -include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -8,16 +7,16 @@ def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do +FileUtils.chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') -<% unless options.skip_yarn? -%> +<% unless options.skip_javascript? -%> - # Install JavaScript dependencies if using Yarn + # Install JavaScript dependencies # system('bin/yarn') <% end -%> <% unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt index 99c2430bc6..03b77d0d46 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -1,5 +1,4 @@ require 'fileutils' -include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -8,16 +7,16 @@ def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do +FileUtils.chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') -<% unless options.skip_yarn? -%> +<% unless options.skip_javascript? -%> - # Install JavaScript dependencies if using Yarn + # Install JavaScript dependencies # system('bin/yarn') <% end -%> <% unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt index 8e53156c71..f69dc91b92 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt @@ -2,7 +2,7 @@ development: adapter: async test: - adapter: async + adapter: test production: adapter: redis diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt index 97f9a92ff3..f39593372c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt @@ -1,4 +1,4 @@ -# MySQL. Versions 5.1.10 and up are supported. +# MySQL. Versions 5.5.8 and up are supported. # # Install the MySQL driver: # gem install activerecord-jdbcmysql-adapter diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt index 1dc508b14f..b6c2e7448a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt @@ -1,4 +1,4 @@ -# MySQL. Versions 5.1.10 and up are supported. +# MySQL. Versions 5.5.8 and up are supported. # # Install the MySQL driver # gem install mysql2 @@ -11,7 +11,7 @@ # default: &default adapter: mysql2 - encoding: utf8 + encoding: utf8mb4 pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index d646694477..08befd9196 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -23,12 +23,7 @@ Rails.application.configure do config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? <%- unless options.skip_sprockets? -%> - <%- if options.skip_javascript? -%> - # Compress CSS. - <%- else -%> - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier - <%- end -%> + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. @@ -69,7 +64,7 @@ Rails.application.configure do # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" + # config.active_job.queue_name_prefix = "<%= app_name %>_production" <%- unless options.skip_action_mailer? -%> config.action_mailer.perform_caching = false diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index 82f2a8aebe..223aa56187 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -21,6 +21,7 @@ Rails.application.configure do # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index 51196ae743..fe48fc34ee 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -5,10 +5,6 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path -<%- unless options[:skip_yarn] -%> -# Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('node_modules') -<%- end -%> # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt index d3bcaa5ec8..c517b0f96b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt @@ -11,6 +11,10 @@ # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https +<%- unless options[:skip_javascript] -%> +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? +<%- end -%> # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt index 179b97de4a..54eb0cb1d2 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt @@ -8,3 +8,10 @@ # Don't force requests from old versions of IE to be UTF-8 encoded # Rails.application.config.action_view.default_enforce_utf8 = false + +# Embed purpose and expiry metadata inside signed and encrypted +# cookies for increased security. +# +# This option is not backwards compatible with earlier Rails versions. +# It's best enabled when your entire app is migrated and stable on 6.0. +# Rails.application.config.action_dispatch.use_cookies_with_metadata = true diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore.tt b/railties/lib/rails/generators/rails/app/templates/gitignore.tt index 4e114fb1d9..860baa1595 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore.tt +++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt @@ -22,19 +22,14 @@ <% end -%> <% unless skip_active_storage? -%> -# Ignore uploaded files in development +# Ignore uploaded files in development. /storage/* <% if keeps? -%> !/storage/.keep <% end -%> <% end -%> - -<% unless options.skip_yarn? -%> -/node_modules -/yarn-error.log - -<% end -%> <% unless options.api? -%> + /public/assets <% end -%> .byebug_history diff --git a/railties/lib/rails/generators/rails/app/templates/package.json.tt b/railties/lib/rails/generators/rails/app/templates/package.json.tt index 46db57dcbe..7174116989 100644 --- a/railties/lib/rails/generators/rails/app/templates/package.json.tt +++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt @@ -1,5 +1,11 @@ { "name": "<%= app_name %>", "private": true, - "dependencies": {} + "dependencies": { + "rails-ujs": ">=5.2.1"<% unless options[:skip_turbolinks] %>, + "turbolinks": "5.1.1"<% end -%><% unless skip_active_storage? %>, + "activestorage": ">=5.2.1"<% end -%><% unless options[:skip_action_cable] %>, + "actioncable": ">=5.2.1"<% end %> + }, + "version": "0.1.0" } diff --git a/railties/lib/rails/generators/rails/assets/USAGE b/railties/lib/rails/generators/rails/assets/USAGE index d2e5ed4482..ee73d05808 100644 --- a/railties/lib/rails/generators/rails/assets/USAGE +++ b/railties/lib/rails/generators/rails/assets/USAGE @@ -5,16 +5,13 @@ Description: To create an asset within a folder, specify the asset's name as a path like 'parent/name'. - This generates a JavaScript stub in app/assets/javascripts and a stylesheet - stub in app/assets/stylesheets. + This generates a stylesheet stub in app/assets/stylesheets. - If CoffeeScript is available, JavaScripts will be generated with the .coffee extension. If Sass 3 is available, stylesheets will be generated with the .scss extension. Example: `rails generate assets posts` Posts assets. - JavaScript: app/assets/javascripts/posts.js Stylesheet: app/assets/stylesheets/posts.css diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb index ffb695a1f3..9ce8570172 100644 --- a/railties/lib/rails/generators/rails/assets/assets_generator.rb +++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb @@ -3,22 +3,14 @@ module Rails module Generators class AssetsGenerator < NamedBase # :nodoc: - class_option :javascripts, type: :boolean, desc: "Generate JavaScripts" class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" - - class_option :javascript_engine, desc: "Engine for JavaScripts" class_option :stylesheet_engine, desc: "Engine for Stylesheets" private - def asset_name file_name end - hook_for :javascript_engine do |javascript_engine| - invoke javascript_engine, [name] if options[:javascripts] - end - hook_for :stylesheet_engine do |stylesheet_engine| invoke stylesheet_engine, [name] if options[:stylesheets] end diff --git a/railties/lib/rails/generators/rails/assets/templates/javascript.js b/railties/lib/rails/generators/rails/assets/templates/javascript.js deleted file mode 100644 index dee720facd..0000000000 --- a/railties/lib/rails/generators/rails/assets/templates/javascript.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 8cc42325bb..239b3a5739 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -88,7 +88,7 @@ task default: :test PASSTHROUGH_OPTIONS = [ :skip_active_record, :skip_active_storage, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database, - :javascript, :skip_yarn, :api, :quiet, :pretend, :skip + :api, :quiet, :pretend, :skip ] def generate_test_dummy(force = false) @@ -98,6 +98,7 @@ task default: :test opts[:skip_listen] = true opts[:skip_git] = true opts[:skip_turbolinks] = true + opts[:skip_webpack_install] = true opts[:dummy_app] = true invoke Rails::Generators::AppGenerator, @@ -113,7 +114,7 @@ task default: :test end def test_dummy_assets - template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true + template "rails/javascripts.js", "#{dummy_path}/app/javascript/packs/application.js", force: true template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true end diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt index 9a8c4bf098..405642c850 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt @@ -4,21 +4,30 @@ $:.push File.expand_path("lib", __dir__) require "<%= namespaced_name %>/version" # Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "<%= name %>" - s.version = <%= camelized_modules %>::VERSION - s.authors = ["<%= author %>"] - s.email = ["<%= email %>"] - s.homepage = "TODO" - s.summary = "TODO: Summary of <%= camelized_modules %>." - s.description = "TODO: Description of <%= camelized_modules %>." - s.license = "MIT" +Gem::Specification.new do |spec| + spec.name = "<%= name %>" + spec.version = <%= camelized_modules %>::VERSION + spec.authors = ["<%= author %>"] + spec.email = ["<%= email %>"] + spec.homepage = "TODO" + spec.summary = "TODO: Summary of <%= camelized_modules %>." + spec.description = "TODO: Description of <%= camelized_modules %>." + spec.license = "MIT" - s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end - <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "<%= Array(rails_version_specifier).join('", "') %>" + spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + + <%= '# ' if options.dev? || options.edge? -%>spec.add_dependency "rails", "<%= Array(rails_version_specifier).join('", "') %>" <% unless options[:skip_active_record] -%> - s.add_development_dependency "<%= gem_for_database[0] %>" + spec.add_development_dependency "<%= gem_for_database[0] %>" <% end -%> end diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt index 7a68da5c4b..0aabf09252 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt @@ -7,7 +7,7 @@ pkg/ <%= dummy_path %>/db/*.sqlite3-journal <% end -%> <%= dummy_path %>/log/*.log -<% unless options[:skip_yarn] -%> +<% unless options[:skip_javascript] -%> <%= dummy_path %>/node_modules/ <%= dummy_path %>/yarn-error.log <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt index 755d19ef5d..4f7a8d3d6e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt @@ -10,8 +10,7 @@ ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __d <% end -%> require "rails/test_help" -# Filter out Minitest backtrace while allowing backtrace from other libraries -# to be shown. +# Filter out the backtrace from minitest while preserving the one from other libraries. Minitest.backtrace_filter = Minitest::BacktraceFilter.new <% unless engine? -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt index f83f5a5c62..15bd7956b6 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt @@ -16,7 +16,7 @@ class <%= class_name.pluralize %>Test < ApplicationSystemTestCase click_on "New <%= class_name.titleize %>" <%- attributes_hash.each do |attr, value| -%> - fill_in "<%= attr.humanize.titleize %>", with: <%= value %> + fill_in "<%= attr.humanize %>", with: <%= value %> <%- end -%> click_on "Create <%= human_name %>" @@ -29,7 +29,7 @@ class <%= class_name.pluralize %>Test < ApplicationSystemTestCase click_on "Edit", match: :first <%- attributes_hash.each do |attr, value| -%> - fill_in "<%= attr.humanize.titleize %>", with: <%= value %> + fill_in "<%= attr.humanize %>", with: <%= value %> <%- end -%> click_on "Update <%= human_name %>" diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index 6ab88bd59f..ec29ad12ba 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -67,6 +67,9 @@ module Rails def run_generator(args = default_arguments, config = {}) capture(:stdout) do args += ["--skip-bundle"] unless args.include? "--dev" + args |= ["--skip-bootsnap"] unless args.include? "--no-skip-bootsnap" + args |= ["--skip-webpack-install"] unless args.include? "--no-skip-webpack-install" + generator_class.start(args, config.reverse_merge(destination_root: destination_root)) end end diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index d5c9973c6b..b8173c8d11 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -41,7 +41,7 @@ module Rails alias inspect to_s def to_html - "<table>".dup.tap do |table| + (+"<table>").tap do |table| properties.each do |(name, value)| table << %(<tr><td class="name">#{CGI.escapeHTML(name.to_s)}</td>) formatted_value = if value.kind_of?(Array) @@ -54,6 +54,10 @@ module Rails table << "</table>" end end + + def to_json + Hash[properties].to_json + end end # The Rails version. diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index b4f4a5922a..50fe176946 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -14,8 +14,16 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: end def properties - @info = Rails::Info.to_html - @page_title = "Properties" + respond_to do |format| + format.html do + @info = Rails::Info.to_html + @page_title = "Properties" + end + + format.json do + render json: Rails::Info.to_json + end + end end def routes diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 0b0e802358..e2d36d7654 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -10,6 +10,8 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: helper_method :part_query, :locale_query + content_security_policy(false) + def index @previews = ActionMailer::Preview.all @page_title = "Mailer Previews" diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 88dd932370..a67b90e285 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -224,7 +224,7 @@ module Rails end def railtie_namespace #:nodoc: - @railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) } + @railtie_namespace ||= self.class.module_parents.detect { |n| n.respond_to?(:railtie_namespace) } end protected diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 2d66a4dc7d..d7170e6282 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -50,7 +50,7 @@ module Rails # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. # Otherwise the string contains just line and text. def to_s(options = {}) - s = "[#{line.to_s.rjust(options[:indent])}] ".dup + s = +"[#{line.to_s.rjust(options[:indent])}] " s << "[#{tag}] " if options[:tag] s << text end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 56f2eba312..2f644a20c9 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -12,6 +12,7 @@ require "rake" middleware misc restart + routes tmp yarn ).tap { |arr| diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake index 65af778a15..3a78de418a 100644 --- a/railties/lib/rails/tasks/annotations.rake +++ b/railties/lib/rails/tasks/annotations.rake @@ -2,20 +2,20 @@ require "rails/source_annotation_extractor" -task :notes do +task notes: :environment do Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning Rails::Command.invoke :notes end namespace :notes do ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| - task annotation.downcase.intern do + task annotation.downcase.intern => :environment do Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning Rails::Command.invoke :notes, ["--annotations", annotation] end end - task :custom do + task custom: :environment do Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning Rails::Command.invoke :notes, ["--annotations", ENV["ANNOTATION"]] end diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake index 5aea6f7dc5..716fb6a331 100644 --- a/railties/lib/rails/tasks/dev.rake +++ b/railties/lib/rails/tasks/dev.rake @@ -1,10 +1,11 @@ # frozen_string_literal: true -require "rails/dev_caching" +require "rails/command" +require "active_support/deprecation" namespace :dev do - desc "Toggle development mode caching on/off" - task :cache do - Rails::DevCaching.enable_by_file + task cache: :environment do + ActiveSupport::Deprecation.warn("Using `bin/rake dev:cache` is deprecated and will be removed in Rails 6.1. Use `bin/rails dev:cache` instead.\n") + Rails::Command.invoke "dev:cache" end end diff --git a/railties/lib/rails/tasks/initializers.rake b/railties/lib/rails/tasks/initializers.rake index ae85cb0f86..f108517d1d 100644 --- a/railties/lib/rails/tasks/initializers.rake +++ b/railties/lib/rails/tasks/initializers.rake @@ -1,8 +1,9 @@ # frozen_string_literal: true -desc "Print out all defined initializers in the order they are invoked by Rails." +require "rails/command" +require "active_support/deprecation" + task initializers: :environment do - Rails.application.initializers.tsort_each do |initializer| - puts "#{initializer.context_class}.#{initializer.name}" - end + ActiveSupport::Deprecation.warn("Using `bin/rake initializers` is deprecated and will be removed in Rails 6.1. Use `bin/rails initializers` instead.\n") + Rails::Command.invoke "initializers" end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake new file mode 100644 index 0000000000..21ce900a8c --- /dev/null +++ b/railties/lib/rails/tasks/routes.rake @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rails/command" +require "active_support/deprecation" + +task routes: :environment do + ActiveSupport::Deprecation.warn("Using `bin/rake routes` is deprecated and will be removed in Rails 6.1. Use `bin/rails routes` instead.\n") + Rails::Command.invoke "routes" +end diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index 594db91eec..8cacf4a49f 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -11,6 +11,7 @@ STATS_DIRECTORIES = [ %w(Mailers app/mailers), %w(Channels app/channels), %w(JavaScripts app/assets/javascripts), + %w(JavaScript app/javascript), %w(Libraries lib/), %w(APIs app/apis), %w(Controller\ tests test/controllers), diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake index cf45a392e8..4fb8586b69 100644 --- a/railties/lib/rails/tasks/yarn.rake +++ b/railties/lib/rails/tasks/yarn.rake @@ -9,7 +9,7 @@ namespace :yarn do rails_env = ENV["RAILS_ENV"] valid_node_envs.include?(rails_env) ? rails_env : "production" end - system({ "NODE_ENV" => node_env }, "./bin/yarn install --no-progress --frozen-lockfile") + system({ "NODE_ENV" => node_env }, "#{Rails.root}/bin/yarn install --no-progress --frozen-lockfile") end end diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb index 2fa7573bdf..d38952bb30 100644 --- a/railties/lib/rails/test_unit/runner.rb +++ b/railties/lib/rails/test_unit/runner.rb @@ -12,8 +12,8 @@ module Rails class << self def attach_before_load_options(opts) - opts.on("--warnings", "-w", "Run with Ruby warnings enabled") {} - opts.on("-e", "--environment ENV", "Run tests in the ENV environment") {} + opts.on("--warnings", "-w", "Run with Ruby warnings enabled") { } + opts.on("-e", "--environment ENV", "Run tests in the ENV environment") { } end def parse_options(argv) @@ -87,7 +87,7 @@ module Rails @filters = [ @named_filter, *derive_line_filters(patterns) ].compact end - # Minitest uses === to find matching filters. + # minitest uses === to find matching filters. def ===(method) @filters.any? { |filter| filter === method } end @@ -96,7 +96,7 @@ module Rails def derive_named_filter(filter) if filter.respond_to?(:named_filter) filter.named_filter - elsif filter =~ %r%/(.*)/% # Regexp filtering copied from Minitest. + elsif filter =~ %r%/(.*)/% # Regexp filtering copied from minitest. Regexp.new $1 elsif filter.is_a?(String) filter diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 6fdb4648c2..4e4a504c97 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -30,6 +30,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/railties/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "actionpack", version diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index b42f37d6b9..9c866263f0 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -21,12 +21,14 @@ end class ActiveSupport::TestCase include ActiveSupport::Testing::Stream - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 4ca6d02b85..46ee0d670e 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -68,20 +68,6 @@ module ApplicationTests assert_equal 'a = "/assets/rails.png";', last_response.body.strip end - test "assets do not require compressors until it is used" do - app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" - add_to_env_config "production", "config.assets.compile = true" - add_to_env_config "production", "config.assets.precompile = []" - - # Load app env - app "production" - - assert_not defined?(Uglifier) - get "/assets/demo.js" - assert_match "alert()", last_response.body - assert defined?(Uglifier) - end - test "precompile creates the file, gives it the original asset's content and run in production as default" do app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" @@ -443,13 +429,13 @@ module ApplicationTests end test "digested assets are not mistakenly removed" do - app_file "app/assets/application.js", "alert();" + app_file "app/assets/application.css", "div { font-weight: bold }" add_to_config "config.assets.compile = true" precompile! - files = Dir["#{app_path}/public/assets/application-*.js"] - assert_equal 1, files.length, "Expected digested application.js asset to be generated, but none found" + files = Dir["#{app_path}/public/assets/application-*.css"] + assert_equal 1, files.length, "Expected digested application.css asset to be generated, but none found" end test "digested assets are removed from configured path" do diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index 54934dbe24..d02100d94c 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -43,18 +43,20 @@ module ApplicationTests # Ignore line that's only output by Bundler < 1.14 output.sub!(/^Resolving dependencies\.\.\.\n/, "") + # Suppress Bundler platform warnings from output + output.gsub!(/^The dependency .* will be unused .*\.\n/, "") - assert_equal(<<-OUTPUT, output) -== Installing dependencies == -The Gemfile's dependencies are satisfied + assert_equal(<<~OUTPUT, output) + == Installing dependencies == + The Gemfile's dependencies are satisfied -== Preparing database == -Created database 'db/development.sqlite3' -Created database 'db/test.sqlite3' + == Preparing database == + Created database 'db/development.sqlite3' + Created database 'db/test.sqlite3' -== Removing old logs and tempfiles == + == Removing old logs and tempfiles == -== Restarting application server == + == Restarting application server == OUTPUT end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index c2699006f6..fa418f564b 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -3,6 +3,7 @@ require "isolation/abstract_unit" require "rack/test" require "env_helpers" +require "set" class ::MyMailInterceptor def self.delivering_email(email); email; end @@ -123,6 +124,18 @@ module ApplicationTests assert_equal "MyLogger", Rails.application.config.logger.class.name end + test "raises an error if cache does not support recyclable cache keys" do + build_app(initializers: true) + add_to_env_config "production", "config.cache_store = Class.new {}.new" + add_to_env_config "production", "config.active_record.cache_versioning = true" + + error = assert_raise(RuntimeError) do + app "production" + end + + assert_match(/You're using a cache/, error.message) + end + test "a renders exception on pending migration" do add_to_config <<-RUBY config.active_record.migration_error = :page_load @@ -297,6 +310,53 @@ module ApplicationTests assert_equal %w(noop_email).to_set, PostsMailer.instance_variable_get(:@action_methods) end + test "does not eager load attribute methods in development" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + app "development" + + assert_not_includes Post.instance_methods, :title + end + + test "eager loads attribute methods in production" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app "production" + + assert_includes Post.instance_methods, :title + end + test "initialize an eager loaded, cache classes app" do add_to_config <<-RUBY config.eager_load = true @@ -1671,7 +1731,7 @@ module ApplicationTests test "config_for loads custom configuration from yaml files" do app_file "config/custom.yml", <<-RUBY development: - key: 'custom key' + foo: 'bar' RUBY add_to_config <<-RUBY @@ -1680,7 +1740,54 @@ module ApplicationTests app "development" - assert_equal "custom key", Rails.application.config.my_custom_config["key"] + assert_equal "bar", Rails.application.config.my_custom_config["foo"] + end + + test "config_for loads custom configuration from yaml accessible as symbol" do + app_file "config/custom.yml", <<-RUBY + development: + foo: 'bar' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + app "development" + + assert_equal "bar", Rails.application.config.my_custom_config[:foo] + end + + test "config_for loads custom configuration from yaml accessible as method" do + app_file "config/custom.yml", <<-RUBY + development: + foo: 'bar' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + app "development" + + assert_equal "bar", Rails.application.config.my_custom_config.foo + end + + test "config_for loads nested custom configuration from yaml as symbol keys" do + app_file "config/custom.yml", <<-RUBY + development: + foo: + bar: + baz: 1 + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + app "development" + + assert_equal 1, Rails.application.config.my_custom_config.foo[:bar][:baz] end test "config_for uses the Pathname object if it is provided" do @@ -1996,6 +2103,33 @@ module ApplicationTests assert_equal false, ActionView::Template.finalize_compiled_template_methods end + test "ActiveRecord::Base.filter_attributes should equal to filter_parameters" do + app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY + Rails.application.config.filter_parameters += [ :password, :credit_card_number ] + RUBY + app "development" + assert_equal [ :password, :credit_card_number ], Rails.application.config.filter_parameters + assert_equal [ :password, :credit_card_number ], ActiveRecord::Base.filter_attributes + end + + test "ActiveStorage.routes_prefix can be configured via config.active_storage.routes_prefix" do + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.active_storage.routes_prefix = '/files' + end + RUBY + + output = rails("routes", "-g", "active_storage") + assert_equal <<~MESSAGE, output + Prefix Verb URI Pattern Controller#Action + rails_service_blob GET /files/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_representation GET /files/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /files/disk/:encoded_key/*filename(.:format) active_storage/disk#show + update_rails_disk_service PUT /files/disk/:encoded_token(.:format) active_storage/disk#update + rails_direct_uploads POST /files/direct_uploads(.:format) active_storage/direct_uploads#create + MESSAGE + end + private def force_lazy_load_hooks yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it. diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 4a14042cd3..29f8b1e3d9 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -109,7 +109,7 @@ class FullStackConsoleTest < ActiveSupport::TestCase CODE system "#{app_path}/bin/rails runner 'Post.connection.create_table :posts'" - @master, @slave = PTY.open + @primary, @replica = PTY.open end def teardown @@ -117,19 +117,19 @@ class FullStackConsoleTest < ActiveSupport::TestCase end def write_prompt(command, expected_output = nil) - @master.puts command - assert_output command, @master - assert_output expected_output, @master if expected_output - assert_output "> ", @master + @primary.puts command + assert_output command, @primary + assert_output expected_output, @primary if expected_output + assert_output "> ", @primary end def spawn_console(options) Process.spawn( "#{app_path}/bin/rails console #{options}", - in: @slave, out: @slave, err: @slave + in: @replica, out: @replica, err: @replica ) - assert_output "> ", @master, 30 + assert_output "> ", @primary, 30 end def test_sandbox @@ -138,14 +138,14 @@ class FullStackConsoleTest < ActiveSupport::TestCase write_prompt "Post.count", "=> 0" write_prompt "Post.create" write_prompt "Post.count", "=> 1" - @master.puts "quit" + @primary.puts "quit" spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.transaction { Post.create; raise }" write_prompt "Post.count", "=> 0" - @master.puts "quit" + @primary.puts "quit" end def test_environment_option_and_irb_option @@ -153,6 +153,6 @@ class FullStackConsoleTest < ActiveSupport::TestCase write_prompt "a = 1", "a = 1" write_prompt "puts Rails.env", "puts Rails.env\r\ntest" - @master.puts "quit" + @primary.puts "quit" end end diff --git a/railties/test/application/dbconsole_test.rb b/railties/test/application/dbconsole_test.rb index 8eb293c179..8c03fe4ac6 100644 --- a/railties/test/application/dbconsole_test.rb +++ b/railties/test/application/dbconsole_test.rb @@ -33,11 +33,11 @@ module ApplicationTests end RUBY - master, slave = PTY.open - spawn_dbconsole(slave) - assert_output("sqlite>", master) + primary, replica = PTY.open + spawn_dbconsole(replica) + assert_output("sqlite>", primary) ensure - master.puts ".exit" + primary.puts ".exit" end def test_respect_environment_option @@ -56,14 +56,14 @@ module ApplicationTests database: db/production.sqlite3 YAML - master, slave = PTY.open - spawn_dbconsole(slave, "-e production") - assert_output("sqlite>", master) + primary, replica = PTY.open + spawn_dbconsole(replica, "-e production") + assert_output("sqlite>", primary) - master.puts "pragma database_list;" - assert_output("production.sqlite3", master) + primary.puts "pragma database_list;" + assert_output("production.sqlite3", primary) ensure - master.puts ".exit" + primary.puts ".exit" end private diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 889ad16fb8..bfa66770bd 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -371,6 +371,72 @@ class LoadingTest < ActiveSupport::TestCase end end + test "active record query cache hooks are installed before first request in production" do + app_file "app/controllers/omg_controller.rb", <<-RUBY + begin + class OmgController < ActionController::Metal + ActiveSupport.run_load_hooks(:action_controller, self) + def show + if ActiveRecord::Base.connection.query_cache_enabled + self.response_body = ["Query cache is enabled."] + else + self.response_body = ["Expected ActiveRecord::Base.connection.query_cache_enabled to be true"] + end + end + end + rescue => e + puts "Error loading metal: \#{e.class} \#{e.message}" + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/:controller(/:action)" + end + RUBY + + boot_app "production" + + require "rack/test" + extend Rack::Test::Methods + + get "/omg/show" + assert_equal "Query cache is enabled.", last_response.body + end + + test "active record query cache hooks are installed before first request in development" do + app_file "app/controllers/omg_controller.rb", <<-RUBY + begin + class OmgController < ActionController::Metal + ActiveSupport.run_load_hooks(:action_controller, self) + def show + if ActiveRecord::Base.connection.query_cache_enabled + self.response_body = ["Query cache is enabled."] + else + self.response_body = ["Expected ActiveRecord::Base.connection.query_cache_enabled to be true"] + end + end + end + rescue => e + puts "Error loading metal: \#{e.class} \#{e.message}" + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/:controller(/:action)" + end + RUBY + + boot_app "development" + + require "rack/test" + extend Rack::Test::Methods + + get "/omg/show" + assert_equal "Query cache is enabled.", last_response.body + end + private def setup_ar! @@ -382,4 +448,12 @@ class LoadingTest < ActiveSupport::TestCase end end end + + def boot_app(env = "development") + ENV["RAILS_ENV"] = env + + require "#{app_path}/config/environment" + ensure + ENV.delete "RAILS_ENV" + end end diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb index ecb4ee3446..fe48ef3f03 100644 --- a/railties/test/application/middleware/cookies_test.rb +++ b/railties/test/application/middleware/cookies_test.rb @@ -110,14 +110,14 @@ module ApplicationTests assert_equal "signed cookie".inspect, last_response.body get "/foo/read_raw_cookie" - assert_equal "signed cookie", verifier_sha512.verify(last_response.body) + assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie") get "/foo/write_raw_cookie_sha256" get "/foo/read_signed" assert_equal "signed cookie".inspect, last_response.body get "/foo/read_raw_cookie" - assert_equal "signed cookie", verifier_sha512.verify(last_response.body) + assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie") end test "encrypted cookies rotating multiple encryption keys" do @@ -180,14 +180,14 @@ module ApplicationTests assert_equal "encrypted cookie".inspect, last_response.body get "/foo/read_raw_cookie" - assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body) + assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie") - get "/foo/write_raw_cookie_sha256" + get "/foo/write_raw_cookie_two" get "/foo/read_encrypted" assert_equal "encrypted cookie".inspect, last_response.body get "/foo/read_raw_cookie" - assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body) + assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie") end end end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 9182a63ab7..b25e56b625 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -183,7 +183,7 @@ module ApplicationTests encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" - assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] + assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] end test "session upgrading signature to encryption cookie store works the same way as encrypted cookie store" do @@ -235,7 +235,7 @@ module ApplicationTests encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" - assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] + assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] end test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do @@ -297,7 +297,7 @@ module ApplicationTests encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" - assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] + assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] end test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do @@ -364,7 +364,7 @@ module ApplicationTests encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" - assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] + assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] ensure ENV["RAILS_ENV"] = old_rails_env end @@ -428,7 +428,7 @@ module ApplicationTests verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token) get "/foo/read_raw_cookie" - assert_equal 2, verifier.verify(last_response.body)["foo"] + assert_equal 2, verifier.verify(last_response.body, purpose: "cookie._myapp_session")["foo"] ensure ENV["RAILS_ENV"] = old_rails_env end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 5efaf841d4..631f5bac7f 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -25,6 +25,7 @@ module ApplicationTests boot! assert_equal [ + "Webpacker::DevServerProxy", "Rack::Sendfile", "ActionDispatch::Static", "ActionDispatch::Executor", @@ -56,6 +57,7 @@ module ApplicationTests boot! assert_equal [ + "Webpacker::DevServerProxy", "Rack::Sendfile", "ActionDispatch::Static", "ActionDispatch::Executor", @@ -138,7 +140,7 @@ module ApplicationTests add_to_config "config.ssl_options = { redirect: { host: 'example.com' } }" boot! - assert_equal [{ redirect: { host: "example.com" } }], Rails.application.middleware.first.args + assert_equal [{ redirect: { host: "example.com" } }], Rails.application.middleware[1].args end test "removing Active Record omits its middleware" do @@ -222,30 +224,31 @@ module ApplicationTests test "insert middleware after" do add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config" boot! - assert_equal "Rack::Config", middleware.second + assert_equal "Rack::Config", middleware.third end test "unshift middleware" do add_to_config "config.middleware.unshift Rack::Config" boot! - assert_equal "Rack::Config", middleware.first + assert_equal "Rack::Config", middleware.second end test "Rails.cache does not respond to middleware" do add_to_config "config.cache_store = :memory_store" boot! - assert_equal "Rack::Runtime", middleware.fourth + assert_equal "Rack::Runtime", middleware.fifth end test "Rails.cache does respond to middleware" do boot! - assert_equal "Rack::Runtime", middleware.fifth + assert_equal "ActiveSupport::Cache::Strategy::LocalCache", middleware.fifth + assert_equal "Rack::Runtime", middleware[5] end test "insert middleware before" do add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config" boot! - assert_equal "Rack::Config", middleware.first + assert_equal "Rack::Config", middleware.second end test "can't change middleware after it's built" do diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 0594236b1f..039987ac8c 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -31,6 +31,7 @@ module ApplicationTests output = rails("db:create") assert_match(/Created database/, output) assert File.exist?(expected_database) + yield if block_given? assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded output = rails("db:drop") assert_match(/Dropped database/, output) @@ -52,17 +53,26 @@ module ApplicationTests test "db:create and db:drop respect environment setting" do app_file "config/database.yml", <<-YAML development: - database: <%= Rails.application.config.database %> + database: db/development.sqlite3 adapter: sqlite3 YAML app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do - config.database = "db/development.sqlite3" + config.read_encrypted_secrets = true end RUBY - db_create_and_drop "db/development.sqlite3", environment_loaded: false + app_file "lib/tasks/check_env.rake", <<-RUBY + Rake::Task["db:create"].enhance do + File.write("tmp/config_value", Rails.application.config.read_encrypted_secrets) + end + RUBY + + db_create_and_drop("db/development.sqlite3", environment_loaded: false) do + assert File.exist?("tmp/config_value") + assert_equal "true", File.read("tmp/config_value") + end end def with_database_existing @@ -93,7 +103,7 @@ module ApplicationTests test "db:create failure because bad permissions" do with_bad_permissions do output = rails("db:create", allow_failure: true) - assert_match(/Couldn't create database/, output) + assert_match("Couldn't create '#{database_url_db_name}' database. Please check your configuration.", output) assert_equal 1, $?.exitstatus end end diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb index 66e1ac9d99..a87f453075 100644 --- a/railties/test/application/rake/dev_test.rb +++ b/railties/test/application/rake/dev_test.rb @@ -9,6 +9,7 @@ module ApplicationTests def setup build_app + add_to_env_config("development", "config.active_support.deprecation = :stderr") end def teardown @@ -17,33 +18,46 @@ module ApplicationTests test "dev:cache creates file and outputs message" do Dir.chdir(app_path) do - output = rails("dev:cache") - assert File.exist?("tmp/caching-dev.txt") - assert_match(/Development mode is now being cached/, output) + stderr = capture(:stderr) do + output = run_rake_dev_cache + assert File.exist?("tmp/caching-dev.txt") + assert_match(/Development mode is now being cached/, output) + end + assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr) end end test "dev:cache deletes file and outputs message" do Dir.chdir(app_path) do - rails "dev:cache" # Create caching file. - output = rails("dev:cache") # Delete caching file. - assert_not File.exist?("tmp/caching-dev.txt") - assert_match(/Development mode is no longer being cached/, output) + stderr = capture(:stderr) do + run_rake_dev_cache # Create caching file. + output = run_rake_dev_cache # Delete caching file. + assert_not File.exist?("tmp/caching-dev.txt") + assert_match(/Development mode is no longer being cached/, output) + end + assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr) end end test "dev:cache touches tmp/restart.txt" do Dir.chdir(app_path) do - rails "dev:cache" - assert File.exist?("tmp/restart.txt") - - prev_mtime = File.mtime("tmp/restart.txt") - sleep(1) - rails "dev:cache" - curr_mtime = File.mtime("tmp/restart.txt") - assert_not_equal prev_mtime, curr_mtime + stderr = capture(:stderr) do + run_rake_dev_cache + assert File.exist?("tmp/restart.txt") + + prev_mtime = File.mtime("tmp/restart.txt") + run_rake_dev_cache + curr_mtime = File.mtime("tmp/restart.txt") + assert_not_equal prev_mtime, curr_mtime + end + assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr) end end + + private + def run_rake_dev_cache + `bin/rake dev:cache` + end end end end diff --git a/railties/test/application/rake/initializers_test.rb b/railties/test/application/rake/initializers_test.rb new file mode 100644 index 0000000000..8de4967021 --- /dev/null +++ b/railties/test/application/rake/initializers_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeInitializersTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rake initializers` prints out defined initializers invoked by Rails" do + capture(:stderr) do + initial_output = run_rake_initializers + initial_output_length = initial_output.split("\n").length + + assert_operator initial_output_length, :>, 0 + assert_not initial_output.include?("set_added_test_module") + + add_to_config <<-RUBY + initializer(:set_added_test_module) { } + RUBY + + final_output = run_rake_initializers + final_output_length = final_output.split("\n").length + + assert_equal 1, (final_output_length - initial_output_length) + assert final_output.include?("set_added_test_module") + end + end + + test "`rake initializers` outputs a deprecation warning" do + add_to_env_config("development", "config.active_support.deprecation = :stderr") + + stderr = capture(:stderr) { run_rake_initializers } + assert_match(/DEPRECATION WARNING: Using `bin\/rake initializers` is deprecated and will be removed in Rails 6.1/, stderr) + end + + private + def run_rake_initializers + Dir.chdir(app_path) { `bin/rake initializers` } + end + end + end +end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index 07d96fcb56..ef99365e75 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -16,21 +16,24 @@ module ApplicationTests teardown_app end - def db_create_and_drop(namespace, expected_database, environment_loaded: true) + def db_create_and_drop(namespace, expected_database) Dir.chdir(app_path) do output = rails("db:create") assert_match(/Created database/, output) assert_match_namespace(namespace, output) + assert_no_match(/already exists/, output) assert File.exist?(expected_database) + output = rails("db:drop") assert_match(/Dropped database/, output) assert_match_namespace(namespace, output) + assert_no_match(/does not exist/, output) assert_not File.exist?(expected_database) end end - def db_create_and_drop_namespace(namespace, expected_database, environment_loaded: true) + def db_create_and_drop_namespace(namespace, expected_database) Dir.chdir(app_path) do output = rails("db:create:#{namespace}") assert_match(/Created database/, output) @@ -52,11 +55,40 @@ module ApplicationTests end end - def db_migrate_and_schema_dump_and_load(namespace, expected_database, format) + def db_migrate_and_migrate_status + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + output = rails "db:migrate:status" + assert_match(/up \d+ Create books/, output) + assert_match(/up \d+ Create dogs/, output) + end + end + + def db_migrate_and_schema_cache_dump Dir.chdir(app_path) do - rails "generate", "model", "book", "title:string" - rails "generate", "model", "dog", "name:string" - write_models_for_animals + generate_models_for_animals + rails "db:migrate" + rails "db:schema:cache:dump" + assert File.exist?("db/schema_cache.yml") + assert File.exist?("db/animals_schema_cache.yml") + end + end + + def db_migrate_and_schema_cache_dump_and_schema_cache_clear + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + rails "db:schema:cache:dump" + rails "db:schema:cache:clear" + assert_not File.exist?("db/schema_cache.yml") + assert_not File.exist?("db/animals_schema_cache.yml") + end + end + + def db_migrate_and_schema_dump_and_load(format) + Dir.chdir(app_path) do + generate_models_for_animals rails "db:migrate", "db:#{format}:dump" if format == "schema" @@ -81,11 +113,9 @@ module ApplicationTests end end - def db_migrate_namespaced(namespace, expected_database) + def db_migrate_namespaced(namespace) Dir.chdir(app_path) do - rails "generate", "model", "book", "title:string" - rails "generate", "model", "dog", "name:string" - write_models_for_animals + generate_models_for_animals output = rails("db:migrate:#{namespace}") if namespace == "primary" assert_match(/CreateBooks: migrated/, output) @@ -95,6 +125,18 @@ module ApplicationTests end end + def db_migrate_status_namespaced(namespace) + Dir.chdir(app_path) do + generate_models_for_animals + output = rails("db:migrate:status:#{namespace}") + if namespace == "primary" + assert_match(/up \d+ Create books/, output) + else + assert_match(/up \d+ Create dogs/, output) + end + end + end + def write_models_for_animals # make a directory for the animals migration FileUtils.mkdir_p("#{app_path}/db/animals_migrate") @@ -114,51 +156,75 @@ module ApplicationTests # create the base model for dog to inherit from File.open("#{app_path}/app/models/animals_base.rb", "w") do |file| - file.write(<<-EOS -class AnimalsBase < ActiveRecord::Base - self.abstract_class = true + file.write(<<~EOS) + class AnimalsBase < ActiveRecord::Base + self.abstract_class = true - establish_connection :animals -end -EOS -) + establish_connection :animals + end + EOS end end + def generate_models_for_animals + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + end + test "db:create and db:drop works on all databases for env" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_create_and_drop namespace, config["database"] + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_create_and_drop db_config.spec_name, db_config.config["database"] end end test "db:create:namespace and db:drop:namespace works on specified databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_create_and_drop_namespace namespace, config["database"] + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_create_and_drop_namespace db_config.spec_name, db_config.config["database"] end end test "db:migrate and db:schema:dump and db:schema:load works on all databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_migrate_and_schema_dump_and_load namespace, config["database"], "schema" - end + db_migrate_and_schema_dump_and_load "schema" end test "db:migrate and db:structure:dump and db:structure:load works on all databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_migrate_and_schema_dump_and_load namespace, config["database"], "structure" - end + db_migrate_and_schema_dump_and_load "structure" end test "db:migrate:namespace works" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| - db_migrate_namespaced namespace, config["database"] + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_migrate_namespaced db_config.spec_name end end + + test "db:migrate:status works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_migrate_status + end + + test "db:migrate:status:namespace works" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_migrate_namespaced db_config.spec_name + db_migrate_status_namespaced db_config.spec_name + end + end + + test "db:schema:cache:dump works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_cache_dump + end + + test "db:schema:cache:clear works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_cache_dump_and_schema_cache_clear + end end end end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 9e22ba84b5..60802ef7c4 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -10,6 +10,7 @@ module ApplicationTests def setup build_app + add_to_env_config("development", "config.active_support.deprecation = :stderr") require "rails/all" super end diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb new file mode 100644 index 0000000000..2c23ff4679 --- /dev/null +++ b/railties/test/application/rake/routes_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeRoutesTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + setup :build_app + teardown :teardown_app + + test "`rake routes` outputs routes" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + end + RUBY + + assert_equal <<~MESSAGE, run_rake_routes + Prefix Verb URI Pattern Controller#Action + cart GET /cart(.:format) cart#show + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show +rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show +update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create + MESSAGE + end + + test "`rake routes` outputs a deprecation warning" do + add_to_env_config("development", "config.active_support.deprecation = :stderr") + + stderr = capture(:stderr) { run_rake_routes } + assert_match(/DEPRECATION WARNING: Using `bin\/rake routes` is deprecated and will be removed in Rails 6.1/, stderr) + end + + private + def run_rake_routes + Dir.chdir(app_path) { `bin/rake routes` } + end + end + end +end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 1522a2bbc5..c38b91ef03 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -118,7 +118,7 @@ module ApplicationTests end def test_code_statistics_sanity - assert_match "Code LOC: 25 Test LOC: 0 Code to Test Ratio: 1:0.0", + assert_match "Code LOC: 32 Test LOC: 0 Code to Test Ratio: 1:0.0", rails("stats") end @@ -189,7 +189,10 @@ module ApplicationTests rails "generate", "model", "Product" rails "generate", "model", "Cart" rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to" - with_rails_env("test") { rails("db:migrate") } + with_rails_env("test") do + rails("db:migrate") + rails("webpacker:compile") + end output = rails("test") assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output) diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb index 92b991dd05..ab9e910aed 100644 --- a/railties/test/application/server_test.rb +++ b/railties/test/application/server_test.rb @@ -40,17 +40,17 @@ module ApplicationTests f.puts "require 'bundler/setup'" end - master, slave = PTY.open + primary, replica = PTY.open pid = nil begin - pid = Process.spawn("#{app_path}/bin/rails server -P tmp/dummy.pid", in: slave, out: slave, err: slave) - assert_output("Listening", master) + pid = Process.spawn("#{app_path}/bin/rails server -P tmp/dummy.pid", in: replica, out: replica, err: replica) + assert_output("Listening", primary) rails("restart") - assert_output("Restarting", master) - assert_output("Inherited", master) + assert_output("Restarting", primary) + assert_output("Inherited", primary) ensure kill(pid) if pid end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 455dc60efd..0046fec062 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -525,9 +525,18 @@ module ApplicationTests def test_run_in_parallel_with_processes file_name = create_parallel_processes_test_file + app_file "db/schema.rb", <<-RUBY + ActiveRecord::Schema.define(version: 1) do + create_table :users do |t| + t.string :name + end + end + RUBY + output = run_test_command(file_name) assert_match %r{Finished in.*\n2 runs, 2 assertions}, output + assert_no_match "create_table(:users)", output end def test_run_in_parallel_with_threads @@ -539,9 +548,42 @@ module ApplicationTests file_name = create_parallel_threads_test_file + app_file "db/schema.rb", <<-RUBY + ActiveRecord::Schema.define(version: 1) do + create_table :users do |t| + t.string :name + end + end + RUBY + output = run_test_command(file_name) assert_match %r{Finished in.*\n2 runs, 2 assertions}, output + assert_no_match "create_table(:users)", output + end + + def test_run_in_parallel_with_unmarshable_exception + file = app_file "test/fail_test.rb", <<-RUBY + require "test_helper" + class FailTest < ActiveSupport::TestCase + class BadError < StandardError + def initialize + super + @proc = ->{ } + end + end + + test "fail" do + raise BadError + assert true + end + end + RUBY + + output = run_test_command(file) + + assert_match "DRb::DRbRemoteError: FailTest::BadError", output + assert_match "1 runs, 0 assertions, 0 failures, 1 errors", output end def test_raise_error_when_specified_file_does_not_exist diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb index 8490f9eb10..90e084ddca 100644 --- a/railties/test/backtrace_cleaner_test.rb +++ b/railties/test/backtrace_cleaner_test.rb @@ -8,22 +8,6 @@ class BacktraceCleanerTest < ActiveSupport::TestCase @cleaner = Rails::BacktraceCleaner.new end - test "should format installed gems correctly" do - backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ] - result = @cleaner.clean(backtrace, :all) - assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] - end - - test "should format installed gems not in Gem.default_dir correctly" do - target_dir = Gem.path.detect { |p| p != Gem.default_dir } - # skip this test if default_dir is the only directory on Gem.path - if target_dir - backtrace = [ "#{target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] - result = @cleaner.clean(backtrace, :all) - assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] - end - end - test "should consider traces from irb lines as User code" do backtrace = [ "(irb):1", "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'", diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb index 51917de2e0..e763cfb376 100644 --- a/railties/test/code_statistics_calculator_test.rb +++ b/railties/test/code_statistics_calculator_test.rb @@ -26,7 +26,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase end end - test "count number of methods in Minitest file" do + test "count number of methods in minitest file" do code = <<-RUBY class FooTest < ActionController::TestCase test 'expectation' do diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb index 5b8b9e4eda..7842b0db61 100644 --- a/railties/test/commands/credentials_test.rb +++ b/railties/test/commands/credentials_test.rb @@ -55,6 +55,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end + test "edit command modifies file specified by environment option" do + assert_match(/access_key_id: 123/, run_edit_command(environment: "production")) + Dir.chdir(app_path) do + assert File.exist?("config/credentials/production.key") + assert File.exist?("config/credentials/production.yml.enc") + end + end + test "show credentials" do assert_match(/access_key_id: 123/, run_show_command) end @@ -70,17 +78,25 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase remove_file "config/master.key" add_to_config "config.require_master_key = false" - assert_match(/Missing master key to decrypt credentials/, run_show_command) + assert_match(/Missing 'config\/master\.key' to decrypt credentials/, run_show_command) + end + + test "show command displays content specified by environment option" do + run_edit_command(environment: "production") + + assert_match(/access_key_id: 123/, run_show_command(environment: "production")) end private - def run_edit_command(editor: "cat") + def run_edit_command(editor: "cat", environment: nil, **options) switch_env("EDITOR", editor) do - rails "credentials:edit" + args = environment ? ["--environment", environment] : [] + rails "credentials:edit", args, **options end end - def run_show_command(**options) - rails "credentials:show", **options + def run_show_command(environment: nil, **options) + args = environment ? ["--environment", environment] : [] + rails "credentials:show", args, **options end end diff --git a/railties/test/commands/dev_test.rb b/railties/test/commands/dev_test.rb new file mode 100644 index 0000000000..ae8516fe9a --- /dev/null +++ b/railties/test/commands/dev_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" + +class Rails::Command::DevTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rails dev:cache` creates both caching and restart file when restart file doesn't exist and dev caching is currently off" do + Dir.chdir(app_path) do + assert_not File.exist?("tmp/caching-dev.txt") + assert_not File.exist?("tmp/restart.txt") + + assert_equal <<~OUTPUT, run_dev_cache_command + Development mode is now being cached. + OUTPUT + + assert File.exist?("tmp/caching-dev.txt") + assert File.exist?("tmp/restart.txt") + end + end + + test "`rails dev:cache` creates caching file and touches restart file when dev caching is currently off" do + Dir.chdir(app_path) do + app_file("tmp/restart.txt", "") + + assert_not File.exist?("tmp/caching-dev.txt") + assert File.exist?("tmp/restart.txt") + restart_file_time_before = File.mtime("tmp/restart.txt") + + assert_equal <<~OUTPUT, run_dev_cache_command + Development mode is now being cached. + OUTPUT + + assert File.exist?("tmp/caching-dev.txt") + restart_file_time_after = File.mtime("tmp/restart.txt") + assert_operator restart_file_time_before, :<, restart_file_time_after + end + end + + test "`rails dev:cache` removes caching file and touches restart file when dev caching is currently on" do + Dir.chdir(app_path) do + app_file("tmp/caching-dev.txt", "") + app_file("tmp/restart.txt", "") + + assert File.exist?("tmp/caching-dev.txt") + assert File.exist?("tmp/restart.txt") + restart_file_time_before = File.mtime("tmp/restart.txt") + + assert_equal <<~OUTPUT, run_dev_cache_command + Development mode is no longer being cached. + OUTPUT + + assert_not File.exist?("tmp/caching-dev.txt") + restart_file_time_after = File.mtime("tmp/restart.txt") + assert_operator restart_file_time_before, :<, restart_file_time_after + end + end + + private + def run_dev_cache_command + rails "dev:cache" + end +end diff --git a/railties/test/commands/initializers_test.rb b/railties/test/commands/initializers_test.rb new file mode 100644 index 0000000000..bdfbb3021c --- /dev/null +++ b/railties/test/commands/initializers_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" + +class Rails::Command::InitializersTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rails initializers` prints out defined initializers invoked by Rails" do + initial_output = run_initializers_command + initial_output_length = initial_output.split("\n").length + + assert_operator initial_output_length, :>, 0 + assert_not initial_output.include?("set_added_test_module") + + add_to_config <<-RUBY + initializer(:set_added_test_module) { } + RUBY + + final_output = run_initializers_command + final_output_length = final_output.split("\n").length + + assert_equal 1, (final_output_length - initial_output_length) + assert final_output.include?("set_added_test_module") + end + + private + def run_initializers_command + rails "initializers" + end +end diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb index 77ed2bda61..693e532c5b 100644 --- a/railties/test/commands/routes_test.rb +++ b/railties/test/commands/routes_test.rb @@ -13,20 +13,33 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do resource :post + resource :user_permission end RUBY - expected_output = [" Prefix Verb URI Pattern Controller#Action", - " new_post GET /post/new(.:format) posts#new", - "edit_post GET /post/edit(.:format) posts#edit", - " post GET /post(.:format) posts#show", - " PATCH /post(.:format) posts#update", - " PUT /post(.:format) posts#update", - " DELETE /post(.:format) posts#destroy", - " POST /post(.:format) posts#create\n"].join("\n") + expected_post_output = [" Prefix Verb URI Pattern Controller#Action", + " new_post GET /post/new(.:format) posts#new", + "edit_post GET /post/edit(.:format) posts#edit", + " post GET /post(.:format) posts#show", + " PATCH /post(.:format) posts#update", + " PUT /post(.:format) posts#update", + " DELETE /post(.:format) posts#destroy", + " POST /post(.:format) posts#create\n"].join("\n") output = run_routes_command(["-c", "PostController"]) - assert_equal expected_output, output + assert_equal expected_post_output, output + + expected_perm_output = [" Prefix Verb URI Pattern Controller#Action", + " new_user_permission GET /user_permission/new(.:format) user_permissions#new", + "edit_user_permission GET /user_permission/edit(.:format) user_permissions#edit", + " user_permission GET /user_permission(.:format) user_permissions#show", + " PATCH /user_permission(.:format) user_permissions#update", + " PUT /user_permission(.:format) user_permissions#update", + " DELETE /user_permission(.:format) user_permissions#destroy", + " POST /user_permission(.:format) user_permissions#create\n"].join("\n") + + output = run_routes_command(["-c", "UserPermissionController"]) + assert_equal expected_perm_output, output end test "rails routes with global search key" do @@ -64,17 +77,30 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase Rails.application.routes.draw do get '/cart', to: 'cart#show' get '/basketball', to: 'basketball#index' + get '/user_permission', to: 'user_permission#index' end RUBY + expected_cart_output = "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n" output = run_routes_command(["-c", "cart"]) - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + assert_equal expected_cart_output, output output = run_routes_command(["-c", "Cart"]) - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + assert_equal expected_cart_output, output output = run_routes_command(["-c", "CartController"]) - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + assert_equal expected_cart_output, output + + expected_perm_output = [" Prefix Verb URI Pattern Controller#Action", + "user_permission GET /user_permission(.:format) user_permission#index\n"].join("\n") + output = run_routes_command(["-c", "user_permission"]) + assert_equal expected_perm_output, output + + output = run_routes_command(["-c", "UserPermission"]) + assert_equal expected_perm_output, output + + output = run_routes_command(["-c", "UserPermissionController"]) + assert_equal expected_perm_output, output end test "rails routes with namespaced controller search key" do @@ -82,24 +108,40 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase Rails.application.routes.draw do namespace :admin do resource :post + resource :user_permission end end RUBY - expected_output = [" Prefix Verb URI Pattern Controller#Action", - " new_admin_post GET /admin/post/new(.:format) admin/posts#new", - "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", - " admin_post GET /admin/post(.:format) admin/posts#show", - " PATCH /admin/post(.:format) admin/posts#update", - " PUT /admin/post(.:format) admin/posts#update", - " DELETE /admin/post(.:format) admin/posts#destroy", - " POST /admin/post(.:format) admin/posts#create\n"].join("\n") + expected_post_output = [" Prefix Verb URI Pattern Controller#Action", + " new_admin_post GET /admin/post/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", + " admin_post GET /admin/post(.:format) admin/posts#show", + " PATCH /admin/post(.:format) admin/posts#update", + " PUT /admin/post(.:format) admin/posts#update", + " DELETE /admin/post(.:format) admin/posts#destroy", + " POST /admin/post(.:format) admin/posts#create\n"].join("\n") output = run_routes_command(["-c", "Admin::PostController"]) - assert_equal expected_output, output + assert_equal expected_post_output, output output = run_routes_command(["-c", "PostController"]) - assert_equal expected_output, output + assert_equal expected_post_output, output + + expected_perm_output = [" Prefix Verb URI Pattern Controller#Action", + " new_admin_user_permission GET /admin/user_permission/new(.:format) admin/user_permissions#new", + "edit_admin_user_permission GET /admin/user_permission/edit(.:format) admin/user_permissions#edit", + " admin_user_permission GET /admin/user_permission(.:format) admin/user_permissions#show", + " PATCH /admin/user_permission(.:format) admin/user_permissions#update", + " PUT /admin/user_permission(.:format) admin/user_permissions#update", + " DELETE /admin/user_permission(.:format) admin/user_permissions#destroy", + " POST /admin/user_permission(.:format) admin/user_permissions#create\n"].join("\n") + + output = run_routes_command(["-c", "Admin::UserPermissionController"]) + assert_equal expected_perm_output, output + + output = run_routes_command(["-c", "UserPermissionController"]) + assert_equal expected_perm_output, output end test "rails routes displays message when no routes are defined" do diff --git a/railties/test/console_helpers.rb b/railties/test/console_helpers.rb index 8350fce5ee..67f55fdc45 100644 --- a/railties/test/console_helpers.rb +++ b/railties/test/console_helpers.rb @@ -9,7 +9,7 @@ module ConsoleHelpers def assert_output(expected, io, timeout = 10) timeout = Time.now + timeout - output = "".dup + output = +"" until output.include?(expected) || Time.now > timeout if IO.select([io], [], [], 0.1) output << io.read(1) diff --git a/railties/test/credentials_test.rb b/railties/test/credentials_test.rb new file mode 100644 index 0000000000..03370e0fc7 --- /dev/null +++ b/railties/test/credentials_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +class Rails::CredentialsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + setup :build_app + teardown :teardown_app + + test "reads credentials from environment specific path" do + with_credentials do |content, key| + Dir.chdir(app_path) do + Dir.mkdir("config/credentials") + File.write("config/credentials/production.yml.enc", content) + File.write("config/credentials/production.key", key) + end + + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + end + + test "reads credentials from customized path and key" do + with_credentials do |content, key| + Dir.chdir(app_path) do + Dir.mkdir("config/credentials") + File.write("config/credentials/staging.yml.enc", content) + File.write("config/credentials/staging.key", key) + end + + add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')") + add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')") + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + end + + private + def with_credentials + key = "2117e775dc2024d4f49ddf3aeb585919" + # secret_key_base: secret + # mystery: revealed + content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw==" + yield(content, key) + end +end diff --git a/railties/test/engine/commands_test.rb b/railties/test/engine/commands_test.rb index aeb64d445b..0e5167578d 100644 --- a/railties/test/engine/commands_test.rb +++ b/railties/test/engine/commands_test.rb @@ -24,7 +24,7 @@ class Rails::Engine::CommandsTest < ActiveSupport::TestCase def test_runner_command_work_inside_engine output = capture(:stdout) do - Dir.chdir(plugin_path) { system("bin/rails runner 'puts Rails.env'") } + Dir.chdir(plugin_path) { system({ "SKIP_REQUIRE_WEBPACKER" => "true" }, "bin/rails runner 'puts Rails.env'") } end assert_equal "test", output.strip @@ -33,29 +33,29 @@ class Rails::Engine::CommandsTest < ActiveSupport::TestCase def test_console_command_work_inside_engine skip "PTY unavailable" unless available_pty? - master, slave = PTY.open - spawn_command("console", slave) - assert_output(">", master) + primary, replica = PTY.open + spawn_command("console", replica) + assert_output(">", primary) ensure - master.puts "quit" + primary.puts "quit" end def test_dbconsole_command_work_inside_engine skip "PTY unavailable" unless available_pty? - master, slave = PTY.open - spawn_command("dbconsole", slave) - assert_output("sqlite>", master) + primary, replica = PTY.open + spawn_command("dbconsole", replica) + assert_output("sqlite>", primary) ensure - master.puts ".exit" + primary.puts ".exit" end def test_server_command_work_inside_engine skip "PTY unavailable" unless available_pty? - master, slave = PTY.open - pid = spawn_command("server", slave) - assert_output("Listening on", master) + primary, replica = PTY.open + pid = spawn_command("server", replica) + assert_output("Listening on", primary) ensure kill(pid) end @@ -67,6 +67,7 @@ class Rails::Engine::CommandsTest < ActiveSupport::TestCase def spawn_command(command, fd) Process.spawn( + { "SKIP_REQUIRE_WEBPACKER" => "true" }, "#{plugin_path}/bin/rails #{command}", in: fd, out: fd, err: fd ) diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index a54a6dbc28..6d53230eab 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -125,7 +125,7 @@ class ActionsTest < Rails::Generators::TestCase def test_gem_works_even_if_frozen_string_is_passed_as_argument run_generator - action :gem, "frozen_gem".freeze, "1.0.0".freeze + action :gem, -"frozen_gem", -"1.0.0" assert_file "Gemfile", /^gem 'frozen_gem', '1.0.0'$/ end @@ -144,6 +144,44 @@ class ActionsTest < Rails::Generators::TestCase assert_file "Gemfile", /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/ end + def test_github_should_create_an_indented_block + run_generator + + action :github, "user/repo" do + gem "foo" + gem "bar" + gem "baz" + end + + assert_file "Gemfile", /\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/ + end + + def test_github_should_create_an_indented_block_with_options + run_generator + + action :github, "user/repo", a: "correct", other: true do + gem "foo" + gem "bar" + gem "baz" + end + + assert_file "Gemfile", /\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/ + end + + def test_github_should_create_an_indented_block_within_a_group + run_generator + + action :gem_group, :magic do + github "user/repo", a: "correct", other: true do + gem "foo" + gem "bar" + gem "baz" + end + end + + assert_file "Gemfile", /\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n/ + end + def test_environment_should_include_data_in_environment_initializer_block run_generator autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]' diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 9c523ad372..4f2894e71e 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -40,7 +40,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase end assert_file "Gemfile" do |content| - assert_no_match(/gem 'coffee-rails'/, content) assert_no_match(/gem 'sass-rails'/, content) assert_no_match(/gem 'web-console'/, content) assert_no_match(/gem 'capybara'/, content) @@ -118,7 +117,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase app/views/layouts app/views/layouts/mailer.html.erb app/views/layouts/mailer.text.erb - bin/bundle bin/rails bin/rake bin/setup diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index b0f958091c..32d00f3157 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -13,10 +13,11 @@ DEFAULT_APP_FILES = %w( config.ru app/assets/config/manifest.js app/assets/images - app/assets/javascripts - app/assets/javascripts/application.js - app/assets/javascripts/cable.js - app/assets/javascripts/channels + app/javascript + app/javascript/channels + app/javascript/channels/consumer.js + app/javascript/channels/index.js + app/javascript/packs/application.js app/assets/stylesheets app/assets/stylesheets/application.css app/channels/application_cable/channel.rb @@ -37,7 +38,6 @@ DEFAULT_APP_FILES = %w( app/views/layouts/application.html.erb app/views/layouts/mailer.html.erb app/views/layouts/mailer.text.erb - bin/bundle bin/rails bin/rake bin/setup @@ -106,7 +106,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_skip_bundle - assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do + assert_not_called(generator([destination_root], skip_bundle: true, skip_webpack_install: true), :bundle_command) do quietly { generator.invoke_all } # skip_bundle is only about running bundle install, ensure the Gemfile is still # generated. @@ -118,9 +118,9 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track': 'reload'/) - assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track': 'reload'/) + assert_file("app/views/layouts/application.html.erb", /javascript_pack_tag\s+'application', 'data-turbolinks-track': 'reload'/) assert_file("app/assets/stylesheets/application.css") - assert_file("app/assets/javascripts/application.js") + assert_file("app/javascript/packs/application.js") end def test_application_job_file_present @@ -211,23 +211,33 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_new_application_doesnt_need_defaults + run_generator assert_no_file "config/initializers/new_framework_defaults_6_0.rb" end def test_new_application_load_defaults app_root = File.join(destination_root, "myfirstapp") run_generator [app_root] + output = nil assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/ Dir.chdir(app_root) do - output = `./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"` + output = `SKIP_REQUIRE_WEBPACKER=true ./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"` end assert_equal "false\n", output end + def test_csp_initializer_include_connect_src_example + run_generator + + assert_file "config/initializers/content_security_policy.rb" do |content| + assert_match(/# policy\.connect_src/, content) + end + end + def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured app_root = File.join(destination_root, "myapp") run_generator [app_root] @@ -298,10 +308,10 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_app_update_does_not_generate_yarn_contents_when_bin_yarn_is_not_used app_root = File.join(destination_root, "myapp") - run_generator [app_root, "--skip-yarn"] + run_generator [app_root, "--skip-javascript"] stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_yarn: true }, { destination_root: app_root, shell: @shell } + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_javascript: true }, { destination_root: app_root, shell: @shell } generator.send(:app_const) quietly { generator.send(:update_bin_files) } @@ -493,7 +503,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcmysql-adapter" else - assert_gem "mysql2", "'>= 0.4.4', '< 0.6.0'" + assert_gem "mysql2", "'>= 0.4.4'" end end @@ -560,7 +570,6 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator assert_gem "sass-rails" - assert_gem "uglifier" end def test_action_cable_redis_gems @@ -602,48 +611,14 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_inclusion_of_javascript_runtime - run_generator - if defined?(JRUBY_VERSION) - assert_gem "therubyrhino" - elsif RUBY_PLATFORM =~ /mingw|mswin/ - assert_gem "duktape" - else - assert_file "Gemfile", /# gem 'mini_racer', platforms: :ruby/ - end - end - - def test_rails_ujs_is_the_default_ujs_library - run_generator - assert_file "app/assets/javascripts/application.js" do |contents| - assert_match %r{^//= require rails-ujs}, contents - end - end - def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] - assert_no_file "app/assets/javascripts" + assert_no_file "app/javascript" assert_file "app/views/layouts/application.html.erb" do |contents| assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents) - assert_no_match(/javascript_include_tag\s+'application' \%>/, contents) - end - - assert_no_gem "coffee-rails" - assert_no_gem "uglifier" - - assert_file "config/environments/production.rb" do |content| - assert_no_match(/config\.assets\.js_compressor = :uglifier/, content) - end - end - - def test_coffeescript_is_skipped_if_required - run_generator [destination_root, "--skip-coffee"] - - assert_file "Gemfile" do |content| - assert_no_match(/coffee-rails/, content) - assert_match(/uglifier/, content) + assert_no_match(/javascript_pack_tag\s+'application'/, contents) end end @@ -745,7 +720,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_web_console_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) @@ -763,17 +738,23 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_generation_runs_bundle_install - assert_generates_with_bundler + generator([destination_root], skip_webpack_install: true) + + assert_bundler_command_called("install") end def test_dev_option - assert_generates_with_bundler dev: true + generator([destination_root], dev: true, skip_webpack_install: true) + + assert_bundler_command_called("install") rails_path = File.expand_path("../../..", Rails.root) assert_file "Gemfile", /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/ end def test_edge_option - assert_generates_with_bundler edge: true + generator([destination_root], edge: true, skip_webpack_install: true) + + assert_bundler_command_called("install") assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$} end @@ -782,23 +763,18 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "spring" end + def test_bundler_binstub + generator([destination_root], skip_webpack_install: true) + + assert_bundler_command_called("binstubs bundler") + end + def test_spring_binstubs jruby_skip "spring doesn't run on JRuby" - command_check = -> command do - @binstub_called ||= 0 - case command - when "install" - # Called when running bundle, we just want to stub it so nothing to do here. - when "exec spring binstub --all" - @binstub_called += 1 - assert_equal 1, @binstub_called, "exec spring binstub --all expected to be called once, but was called #{@install_called} times." - end - end + generator([destination_root], skip_webpack_install: true) - generator.stub :bundle_command, command_check do - quietly { generator.invoke_all } - end + assert_bundler_command_called("exec spring binstub --all") end def test_spring_no_fork @@ -818,27 +794,30 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_spring_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_no_gem "spring" end - def test_webpack_option + def test_skip_javascript_option command_check = -> command, *_ do @called ||= 0 if command == "webpacker:install" @called += 1 - assert_equal 1, @called, "webpacker:install expected to be called once, but was called #{@called} times." + assert_equal 0, @called, "webpacker:install expected not to be called, but was called #{@called} times." end end - generator([destination_root], webpack: "webpack").stub(:rails_command, command_check) do + generator([destination_root], skip_javascript: true).stub(:rails_command, command_check) do generator.stub :bundle_command, nil do quietly { generator.invoke_all } end end - assert_gem "webpacker" + assert_no_gem "webpacker" + assert_file "config/initializers/content_security_policy.rb" do |content| + assert_no_match(/policy\.connect_src/, content) + end end def test_webpack_option_with_js_framework @@ -860,6 +839,22 @@ class AppGeneratorTest < Rails::Generators::TestCase quietly { generator.invoke_all } end end + + assert_gem "webpacker" + end + + def test_skip_webpack_install + command_check = -> command do + if command == "webpacker:install" + assert false, "webpacker:install expected not to be called." + end + end + + generator([destination_root], skip_webpack_install: true).stub(:rails_command, command_check) do + quietly { generator.invoke_all } + end + + assert_gem "webpacker" end def test_generator_if_skip_turbolinks_is_given @@ -869,13 +864,13 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "app/views/layouts/application.html.erb" do |content| assert_no_match(/data-turbolinks-track/, content) end - assert_file "app/assets/javascripts/application.js" do |content| + assert_file "app/javascript/packs/application.js" do |content| assert_no_match(/turbolinks/, content) end end def test_bootsnap - run_generator + run_generator [destination_root, "--no-skip-bootsnap"] unless defined?(JRUBY_VERSION) assert_gem "bootsnap" @@ -900,7 +895,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_bootsnap_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_no_gem "bootsnap" assert_file "config/boot.rb" do |content| @@ -968,7 +963,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_after_bundle_callback path = "http://example.org/rails_template" - template = %{ after_bundle { run 'echo ran after_bundle' } }.dup + template = +%{ after_bundle { run 'echo ran after_bundle' } } template.instance_eval "def read; self; end" # Make the string respond to read check_open = -> *args do @@ -976,7 +971,7 @@ class AppGeneratorTest < Rails::Generators::TestCase template end - sequence = ["git init", "install", "exec spring binstub --all", "echo ran after_bundle"] + sequence = ["git init", "install", "binstubs bundler", "exec spring binstub --all", "webpacker:install", "echo ran after_bundle"] @sequence_step ||= 0 ensure_bundler_first = -> command, options = nil do assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" @@ -993,7 +988,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - assert_equal 4, @sequence_step + assert_equal 6, @sequence_step end def test_gitignore @@ -1063,18 +1058,14 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def assert_generates_with_bundler(options = {}) - generator([destination_root], options) - + def assert_bundler_command_called(target_command) command_check = -> command do - @install_called ||= 0 + @command_called ||= 0 case command - when "install" - @install_called += 1 - assert_equal 1, @install_called, "install expected to be called once, but was called #{@install_called} times" - when "exec spring binstub --all" - # Called when running tests with spring, let through unscathed. + when target_command + @command_called += 1 + assert_equal 1, @command_called, "#{command} expected to be called once, but was called #{@command_called} times." end end diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb index 3cec41dbf8..83d2429acf 100644 --- a/railties/test/generators/assets_generator_test.rb +++ b/railties/test/generators/assets_generator_test.rb @@ -9,13 +9,11 @@ class AssetsGeneratorTest < Rails::Generators::TestCase def test_assets run_generator - assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end def test_skipping_assets - run_generator ["posts", "--no-stylesheets", "--no-javascripts"] - assert_no_file "app/assets/javascripts/posts.js" + run_generator ["posts", "--no-stylesheets"] assert_no_file "app/assets/stylesheets/posts.css" end end diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb index e543cc11b8..1cb8465539 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -26,8 +26,8 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) end - assert_file "app/assets/javascripts/channels/chat.js" do |channel| - assert_match(/App\.chat = App\.cable\.subscriptions\.create\("ChatChannel/, channel) + assert_file "app/javascript/channels/chat_channel.js" do |channel| + assert_match(/import consumer from "\.\/consumer"\s+consumer\.subscriptions\.create\("ChatChannel/, channel) end end @@ -40,8 +40,8 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_match(/def mute/, channel) end - assert_file "app/assets/javascripts/channels/chat.js" do |channel| - assert_match(/App\.chat = App\.cable\.subscriptions\.create\("ChatChannel/, channel) + assert_file "app/javascript/channels/chat_channel.js" do |channel| + assert_match(/import consumer from "\.\/consumer"\s+consumer\.subscriptions\.create\("ChatChannel/, channel) assert_match(/,\n\n speak/, channel) assert_match(/,\n\n mute: function\(\) \{\n return this\.perform\('mute'\);\n \}\n\}\);/, channel) end @@ -54,15 +54,17 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) end - assert_no_file "app/assets/javascripts/channels/chat.js" + assert_no_file "app/javascript/channels/chat_channel.js" end - def test_cable_js_is_created_if_not_present_already + def test_consumer_js_is_created_if_not_present_already run_generator ["chat"] - FileUtils.rm("#{destination_root}/app/assets/javascripts/cable.js") + FileUtils.rm("#{destination_root}/app/javascript/channels/index.js") + FileUtils.rm("#{destination_root}/app/javascript/channels/consumer.js") run_generator ["camp"] - assert_file "app/assets/javascripts/cable.js" + assert_file "app/javascript/channels/index.js" + assert_file "app/javascript/channels/consumer.js" end def test_channel_on_revoke @@ -70,11 +72,12 @@ class ChannelGeneratorTest < Rails::Generators::TestCase run_generator ["chat"], behavior: :revoke assert_no_file "app/channels/chat_channel.rb" - assert_no_file "app/assets/javascripts/channels/chat.js" + assert_no_file "app/javascript/channels/chat_channel.js" assert_file "app/channels/application_cable/channel.rb" assert_file "app/channels/application_cable/connection.rb" - assert_file "app/assets/javascripts/cable.js" + assert_file "app/javascript/channels/index.js" + assert_file "app/javascript/channels/consumer.js" end def test_channel_suffix_is_not_duplicated @@ -83,7 +86,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_no_file "app/channels/chat_channel_channel.rb" assert_file "app/channels/chat_channel.rb" - assert_no_file "app/assets/javascripts/channels/chat_channel.js" - assert_file "app/assets/javascripts/channels/chat.js" + assert_no_file "app/javascript/channels/chat_channel_channel.js" + assert_file "app/javascript/channels/chat_channel.js" end end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 021004c9b8..8786756c68 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -39,13 +39,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_invokes_assets run_generator - assert_file "app/assets/javascripts/account.js" assert_file "app/assets/stylesheets/account.css" end def test_does_not_invoke_assets_if_required run_generator ["account", "--skip-assets"] - assert_no_file "app/assets/javascripts/account.js" assert_no_file "app/assets/stylesheets/account.css" end @@ -132,9 +130,6 @@ class ControllerGeneratorTest < Rails::Generators::TestCase assert_no_file "app/helpers/account_controller_helper.rb" assert_file "app/helpers/account_helper.rb" - assert_no_file "app/assets/javascripts/account_controller.js" - assert_file "app/assets/javascripts/account.js" - assert_no_file "app/assets/stylesheets/account_controller.css" assert_file "app/assets/stylesheets/account.css" end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index ad2a55f496..25d5dba1d8 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -42,6 +42,21 @@ module GeneratorsTestHelper end end + def with_secondary_database_configuration + original_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = { + test: { + secondary: { + database: "db/secondary.sqlite3", + migrations_paths: "db/secondary_migrate", + }, + }, + } + yield + ensure + ActiveRecord::Base.configurations = original_configurations + end + def copy_routes routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb.tt", __dir__) destination = File.join(destination_root, "config") diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 88a939a55a..5812cbdfc9 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -51,12 +51,12 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end def test_add_migration_with_table_having_from_in_title - migration = "add_email_address_to_blacklisted_from_campaign" + migration = "add_email_address_to_excluded_from_campaign" run_generator [migration, "email_address:string"] assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/add_column :blacklisted_from_campaigns, :email_address, :string/, change) + assert_match(/add_column :excluded_from_campaigns, :email_address, :string/, change) end end end @@ -254,6 +254,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_database_puts_migrations_in_configured_folder + with_secondary_database_configuration do + run_generator ["create_books", "--database=secondary"] + assert_migration "db/secondary_migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books/, change) + end + end + end + end + def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create migration = "delete_books" run_generator [migration, "title:string", "content:text"] diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 8d933e82c3..b06db6dd8a 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -7,6 +7,11 @@ class ModelGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(Account name:string age:integer) + def setup + super + Rails::Generators::ModelHelpers.skip_warn = false + end + def test_help_shows_invoked_generators_options content = run_generator ["--help"] assert_match(/ActiveRecord options:/, content) @@ -37,12 +42,24 @@ class ModelGeneratorTest < Rails::Generators::TestCase end def test_plural_names_are_singularized - content = run_generator ["accounts".freeze] + content = run_generator ["accounts"] assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end + def test_unknown_inflection_rule_are_warned + content = run_generator ["porsche"] + assert_match("[WARNING] Rails cannot recover singular form from its plural form 'porsches'.\nPlease setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb.", content) + assert_file "app/models/porsche.rb", /class Porsche < ApplicationRecord/ + + uncountable_content = run_generator ["sheep"] + assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", uncountable_content) + + regular_content = run_generator ["account"] + assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", regular_content) + end + def test_model_with_underscored_parent_option run_generator ["account", "--parent", "admin/account"] assert_file "app/models/account.rb", /class Account < Admin::Account/ @@ -375,6 +392,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end + def test_database_puts_migrations_in_configured_folder + with_secondary_database_configuration do + run_generator ["account", "--database=secondary"] + assert_migration "db/secondary_migrate/create_accounts.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :accounts/, change) + end + end + end + end + def test_required_belongs_to_adds_required_association run_generator ["account", "supplier:references{required}"] diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 28ac3611b7..66286fc554 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -140,10 +140,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator assert_file "test/dummy/app/assets/stylesheets/application.css" - - assert_file "test/dummy/app/assets/javascripts/application.js" do |contents| - assert_no_match(/jquery/, contents) - end end def test_ensure_that_plugin_options_are_not_passed_to_app_generator @@ -210,28 +206,10 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "#{destination_root}/Gemfile.lock" end - def test_skipping_javascripts_without_mountable_option - run_generator - assert_no_file "app/assets/javascripts/bukkits/application.js" - end - - def test_javascripts_generation - run_generator [destination_root, "--mountable"] - assert_file "app/assets/javascripts/bukkits/application.js" do |content| - assert_match "//= require rails-ujs", content - assert_match "//= require activestorage", content - assert_match "//= require_tree .", content - end - assert_file "app/views/layouts/bukkits/application.html.erb" do |content| - assert_match "javascript_include_tag", content - end - end - - def test_skip_javascripts + def test_skip_javascript run_generator [destination_root, "--skip-javascript", "--mountable"] - assert_no_file "app/assets/javascripts/bukkits/application.js" assert_file "app/views/layouts/bukkits/application.html.erb" do |content| - assert_no_match "javascript_include_tag", content + assert_no_match "javascript_pack_tag", content end end @@ -264,7 +242,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_engine_in_full_mode run_generator [destination_root, "--full"] - assert_file "app/assets/javascripts/bukkits" assert_file "app/assets/stylesheets/bukkits" assert_file "app/assets/images/bukkits" assert_file "app/models" @@ -280,7 +257,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_engine_with_hyphenated_name_in_full_mode run_generator [File.join(destination_root, "hyphenated-name"), "--full"] - assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" + assert_no_file "hyphenated-name/app/assets/javascripts/hyphenated/name" assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" assert_file "hyphenated-name/app/models" @@ -297,7 +274,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_engine_with_hyphenated_and_underscored_name_in_full_mode run_generator [File.join(destination_root, "my_hyphenated-name"), "--full"] - assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" + assert_no_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" assert_file "my_hyphenated-name/app/models" @@ -318,7 +295,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_create_mountable_application_with_mountable_option run_generator [destination_root, "--mountable"] - assert_file "app/assets/javascripts/bukkits" + assert_no_file "app/assets/javascripts/bukkits" assert_file "app/assets/stylesheets/bukkits" assert_file "app/assets/images/bukkits" assert_file "config/routes.rb", /Bukkits::Engine\.routes\.draw do/ @@ -334,7 +311,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_match "<%= csrf_meta_tags %>", contents assert_match "<%= csp_meta_tag %>", contents assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents) assert_match "<%= yield %>", contents end assert_file "test/test_helper.rb" do |content| @@ -348,7 +325,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_create_mountable_application_with_mountable_option_and_hypenated_name run_generator [File.join(destination_root, "hyphenated-name"), "--mountable"] - assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" + assert_no_file "hyphenated-name/app/assets/javascripts/hyphenated/name" assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine\.routes\.draw do/ @@ -364,13 +341,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/app/views/layouts/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Hyphenated name</title>", contents assert_match(/stylesheet_link_tag\s+['"]hyphenated\/name\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents) end end def test_create_mountable_application_with_mountable_option_and_hypenated_and_underscored_name run_generator [File.join(destination_root, "my_hyphenated-name"), "--mountable"] - assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" + assert_no_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine\.routes\.draw do/ @@ -386,13 +363,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/views/layouts/my_hyphenated/name/application.html.erb" do |contents| assert_match "<title>My hyphenated name</title>", contents assert_match(/stylesheet_link_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) end end def test_create_mountable_application_with_mountable_option_and_multiple_hypenates_in_name run_generator [File.join(destination_root, "deep-hyphenated-name"), "--mountable"] - assert_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" + assert_no_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/stylesheets/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/images/deep/hyphenated/name" assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine\.routes\.draw do/ @@ -408,15 +385,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "deep-hyphenated-name/app/views/layouts/deep/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Deep hyphenated name</title>", contents assert_match(/stylesheet_link_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) end end def test_creating_gemspec run_generator - assert_file "bukkits.gemspec", /s\.name\s+= "bukkits"/ - assert_file "bukkits.gemspec", /s\.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ - assert_file "bukkits.gemspec", /s\.version\s+ = Bukkits::VERSION/ + assert_file "bukkits.gemspec", /spec\.name\s+= "bukkits"/ + assert_file "bukkits.gemspec", /spec\.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ + assert_file "bukkits.gemspec", /spec\.version\s+ = Bukkits::VERSION/ end def test_usage_of_engine_commands @@ -737,7 +714,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_after_bundle_callback path = "http://example.org/rails_template" - template = %{ after_bundle { run "echo ran after_bundle" } }.dup + template = +%{ after_bundle { run "echo ran after_bundle" } } template.instance_eval "def read; self; end" # Make the string respond to read check_open = -> *args do diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index 63a2cd3869..b99b4baf6b 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -7,7 +7,11 @@ class ResourceGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(account) - setup :copy_routes + def setup + super + copy_routes + Rails::Generators::ModelHelpers.skip_warn = false + end def test_help_with_inherited_options content = run_generator ["--help"] @@ -61,7 +65,7 @@ class ResourceGeneratorTest < Rails::Generators::TestCase end def test_plural_names_are_singularized - content = run_generator ["accounts".freeze] + content = run_generator ["accounts"] assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) @@ -75,7 +79,7 @@ class ResourceGeneratorTest < Rails::Generators::TestCase end def test_mass_nouns_do_not_throw_warnings - content = run_generator ["sheep".freeze] + content = run_generator ["sheep"] assert_no_match(/\[WARNING\]/, content) end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 3e631f6021..3cffdddd38 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -93,7 +93,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/product_lines.js" assert_file "app/assets/stylesheets/product_lines.css" end @@ -166,7 +165,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_no_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/product_lines.js" assert_no_file "app/assets/stylesheets/product_lines.css" end @@ -222,7 +220,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ - assert_no_file "app/assets/javascripts/product_lines.js" assert_no_file "app/assets/stylesheets/product_lines.css" end @@ -299,7 +296,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ - assert_file "app/assets/javascripts/admin/roles.js" assert_file "app/assets/stylesheets/admin/roles.css" end @@ -335,7 +331,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/admin/roles.js" assert_no_file "app/assets/stylesheets/admin/roles.css" end @@ -380,28 +375,24 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase def test_scaffold_generator_no_assets_with_switch_no_assets run_generator [ "posts", "--no-assets" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_assets_with_switch_assets_false run_generator [ "posts", "--assets=false" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_scaffold_stylesheet_with_switch_no_scaffold_stylesheet run_generator [ "posts", "--no-scaffold-stylesheet" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_scaffold_stylesheet_with_switch_scaffold_stylesheet_false run_generator [ "posts", "--scaffold-stylesheet=false" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end @@ -429,17 +420,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase def test_scaffold_generator_no_stylesheets run_generator [ "posts", "--no-stylesheets" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end - def test_scaffold_generator_no_javascripts - run_generator [ "posts", "--no-javascripts" ] - assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js" - assert_file "app/assets/stylesheets/posts.css" - end - def test_scaffold_generator_outputs_error_message_on_missing_attribute_type run_generator ["post", "title", "body:text", "author"] @@ -452,8 +435,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end - def test_scaffold_generator_belongs_to - run_generator ["account", "name", "currency:belongs_to"] + def test_scaffold_generator_belongs_to_and_references + run_generator ["account", "name", "currency:belongs_to", "user:references"] assert_file "app/models/account.rb", /belongs_to :currency/ @@ -466,7 +449,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/controllers/accounts_controller.rb" do |content| assert_instance_method :account_params, content do |m| - assert_match(/permit\(:name, :currency_id\)/, m) + assert_match(/permit\(:name, :currency_id, :user_id\)/, m) end end @@ -474,6 +457,24 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/^\W{4}<%= form\.text_field :name %>/, content) assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content) end + + assert_file "app/views/accounts/index.html.erb" do |content| + assert_match(/^\W{8}<td><%= account\.name %><\/td>/, content) + assert_match(/^\W{8}<td><%= account\.user_id %><\/td>/, content) + end + + assert_file "app/views/accounts/show.html.erb" do |content| + assert_match(/^\W{2}<%= @account\.name %>/, content) + assert_match(/^\W{2}<%= @account\.user_id %>/, content) + end + end + + def test_scaffold_generator_database + with_secondary_database_configuration do + run_generator ["posts", "--database=secondary"] + + assert_migration "db/secondary_migrate/create_posts.rb" + end end def test_scaffold_generator_password_digest @@ -514,7 +515,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "test/system/users_test.rb" do |content| assert_match(/fill_in "Password", with: 'secret'/, content) - assert_match(/fill_in "Password Confirmation", with: 'secret'/, content) + assert_match(/fill_in "Password confirmation", with: 'secret'/, content) end assert_file "test/fixtures/users.yml" do |content| @@ -622,7 +623,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert File.exist?("app/helpers/bukkits/users_helper.rb") - assert File.exist?("app/assets/javascripts/bukkits/users.js") assert File.exist?("app/assets/stylesheets/bukkits/users.css") end end @@ -652,7 +652,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_not File.exist?("app/helpers/bukkits/users_helper.rb") - assert_not File.exist?("app/assets/javascripts/bukkits/users.js") assert_not File.exist?("app/assets/stylesheets/bukkits/users.css") end end diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index aa577e4234..2dda856f25 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -83,7 +83,7 @@ module SharedGeneratorTests def test_template_is_executed_when_supplied_an_https_path path = "https://gist.github.com/josevalim/103208/raw/" - template = %{ say "It works!" }.dup + template = +%{ say "It works!" } template.instance_eval "def read; self; end" # Make the string respond to read check_open = -> *args do @@ -91,7 +91,7 @@ module SharedGeneratorTests template end - generator([destination_root], template: path).stub(:open, check_open, template) do + generator([destination_root], template: path, skip_webpack_install: true).stub(:open, check_open, template) do generator.stub :bundle_command, nil do quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } end @@ -99,7 +99,7 @@ module SharedGeneratorTests end def test_skip_gemfile - assert_not_called(generator([destination_root], skip_gemfile: true), :bundle_command) do + assert_not_called(generator([destination_root], skip_gemfile: true, skip_webpack_install: true), :bundle_command) do quietly { generator.invoke_all } assert_no_file "Gemfile" end @@ -198,8 +198,10 @@ module SharedGeneratorTests def test_generator_for_active_storage run_generator - assert_file "#{application_path}/app/assets/javascripts/application.js" do |content| - assert_match(/^\/\/= require activestorage/, content) + unless generator_class.name == "Rails::Generators::PluginGenerator" + assert_file "#{application_path}/app/javascript/packs/application.js" do |content| + assert_match(/^import \* as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) + end end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -228,8 +230,8 @@ module SharedGeneratorTests assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ - assert_file "#{application_path}/app/assets/javascripts/application.js" do |content| - assert_no_match(/^\/\/= require activestorage/, content) + assert_file "#{application_path}/app/javascript/packs/application.js" do |content| + assert_no_match(/activestorage/, content) end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -258,8 +260,8 @@ module SharedGeneratorTests assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ - assert_file "#{application_path}/app/assets/javascripts/application.js" do |content| - assert_no_match(/^\/\/= require activestorage/, content) + assert_file "#{application_path}/app/javascript/packs/application.js" do |content| + assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -303,8 +305,8 @@ module SharedGeneratorTests run_generator [destination_root, "--skip-action-cable"] assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ assert_no_file "#{application_path}/config/cable.yml" - assert_no_file "#{application_path}/app/assets/javascripts/cable.js" - assert_no_directory "#{application_path}/app/assets/javascripts/channels" + assert_no_file "#{application_path}/app/javascript/consumer.js" + assert_no_directory "#{application_path}/app/javascript/channels" assert_no_directory "#{application_path}/app/channels" assert_file "Gemfile" do |content| assert_no_match(/redis/, content) @@ -320,8 +322,6 @@ module SharedGeneratorTests assert_file "Gemfile" do |content| assert_no_match(/sass-rails/, content) - assert_no_match(/uglifier/, content) - assert_no_match(/coffee-rails/, content) end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -330,35 +330,21 @@ module SharedGeneratorTests assert_file "#{application_path}/config/environments/production.rb" do |content| assert_no_match(/config\.assets\.digest/, content) - assert_no_match(/config\.assets\.js_compressor/, content) assert_no_match(/config\.assets\.css_compressor/, content) assert_no_match(/config\.assets\.compile/, content) end end def test_generator_for_yarn + skip "#34009 disabled JS by default for plugins" if generator_class.name == "Rails::Generators::PluginGenerator" run_generator assert_file "#{application_path}/package.json", /dependencies/ - assert_file "#{application_path}/config/initializers/assets.rb", /node_modules/ - - assert_file ".gitignore" do |content| - assert_match(/node_modules/, content) - assert_match(/yarn-error\.log/, content) - end + assert_file "#{application_path}/bin/yarn" end def test_generator_for_yarn_skipped - run_generator([destination_root, "--skip-yarn"]) + run_generator([destination_root, "--skip-javascript"]) assert_no_file "#{application_path}/package.json" assert_no_file "#{application_path}/bin/yarn" - - assert_file "#{application_path}/config/initializers/assets.rb" do |content| - assert_no_match(/node_modules/, content) - end - - assert_file ".gitignore" do |content| - assert_no_match(/node_modules/, content) - assert_no_match(/yarn-error\.log/, content) - end end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 516c457e48..d4eed69a87 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -16,6 +16,9 @@ require "active_support/testing/autorun" require "active_support/testing/stream" require "active_support/testing/method_call_assertions" require "active_support/test_case" +require "minitest/retry" + +Minitest::Retry.use!(verbose: false, retry_count: 1) RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) @@ -30,11 +33,11 @@ require "rails/secrets" module TestHelpers module Paths def app_template_path - File.join Dir.tmpdir, "app_template" + File.join RAILS_FRAMEWORK_ROOT, "tmp/templates/app_template" end def tmp_path(*args) - @tmp_path ||= File.realpath(Dir.mktmpdir) + @tmp_path ||= File.realpath(Dir.mktmpdir(nil, File.join(RAILS_FRAMEWORK_ROOT, "tmp"))) File.join(@tmp_path, *args) end @@ -71,7 +74,7 @@ module TestHelpers end def extract_body(response) - "".dup.tap do |body| + (+"").tap do |body| response[2].each { |chunk| body << chunk } end end @@ -124,26 +127,53 @@ module TestHelpers primary: <<: *default database: db/development.sqlite3 + primary_readonly: + <<: *default + database: db/development.sqlite3 + replica: true animals: <<: *default database: db/development_animals.sqlite3 migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + replica: true test: primary: <<: *default database: db/test.sqlite3 + primary_readonly: + <<: *default + database: db/test.sqlite3 + replica: true animals: <<: *default database: db/test_animals.sqlite3 migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + replica: true production: primary: <<: *default database: db/production.sqlite3 + primary_readonly: + <<: *default + database: db/production.sqlite3 + replica: true animals: <<: *default database: db/production_animals.sqlite3 migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + readonly: true YAML end else @@ -170,7 +200,6 @@ module TestHelpers config.eager_load = false config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log - config.active_support.test_order = :random config.action_controller.allow_forgery_protection = false config.log_level = :info RUBY @@ -194,7 +223,6 @@ module TestHelpers @app.config.eager_load = false @app.config.session_store :cookie_store, key: "_myapp_session" @app.config.active_support.deprecation = :log - @app.config.active_support.test_order = :random @app.config.log_level = :info yield @app if block_given? @@ -444,17 +472,28 @@ Module.new do # Build a rails app FileUtils.rm_rf(app_template_path) - FileUtils.mkdir(app_template_path) + FileUtils.mkdir_p(app_template_path) + + Dir.chdir "#{RAILS_FRAMEWORK_ROOT}/actionview" do + `yarn build` + end `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc` File.open("#{app_template_path}/config/boot.rb", "w") do |f| f.puts "require 'rails/all'" end + Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version. + + # Manually install `webpack` as bin symlinks are not created for subdependencies + # in workspaces. See https://github.com/yarnpkg/yarn/issues/4964 + Dir.chdir(app_template_path) { `yarn add webpack@4.17.1 --tilde` } + Dir.chdir(app_template_path) { `yarn add webpack-cli` } + # Fake 'Bundler.require' -- we run using the repo's Gemfile, not an # app-specific one: we don't want to require every gem that lists. contents = File.read("#{app_template_path}/config/application.rb") - contents.sub!(/^Bundler\.require.*/, "%w(turbolinks).each { |r| require r }") + contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }") File.write("#{app_template_path}/config/application.rb", contents) require "rails" diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index 6e8f333e1d..ac37062e6d 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -56,7 +56,7 @@ module Rails end def test_notification - logger = TestLogger.new {} + logger = TestLogger.new { } assert_difference("subscriber.starts.length") do assert_difference("subscriber.finishes.length") do diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 878a238f8d..6ab68f8333 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -50,6 +50,11 @@ class InfoControllerTest < ActionController::TestCase assert_select "table" end + test "info controller renders json with properties" do + get :properties, format: :json + assert_equal Rails::Info.to_json, response.body + end + test "info controller renders with routes" do get :routes assert_response :success diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 50522c1be6..d167a86e56 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -43,6 +43,18 @@ class InfoTest < ActiveSupport::TestCase end end + def test_json_includes_middleware + Rails::Info.module_eval do + property "Middleware", ["Rack::Lock", "Rack::Static"] + end + + hash = JSON.parse(Rails::Info.to_json) + assert_includes hash.keys, "Middleware" + properties.value_for("Middleware").each do |value| + assert_includes hash["Middleware"], value + end + end + private def properties Rails::Info.properties diff --git a/tasks/release.rb b/tasks/release.rb index f13342b90c..1e83814bae 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -105,7 +105,7 @@ namespace :changelog do current_contents = File.read(fname) header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n" - header += "* No changes.\n\n\n" if current_contents =~ /\A##/ + header += "* No changes.\n\n\n" if current_contents.start_with?("##") contents = header + current_contents File.write(fname, contents) end |