diff options
917 files changed, 14734 insertions, 7229 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 1f0f067c5b..d59a0780d1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,3 +1,25 @@ +checks: + argument-count: + enabled: false + complex-logic: + enabled: false + file-lines: + enabled: false + method-complexity: + enabled: false + method-count: + enabled: false + method-lines: + enabled: false + nested-control-flow: + enabled: false + return-statements: + enabled: false + similar-code: + enabled: false + identical-code: + enabled: false + engines: rubocop: enabled: true diff --git a/.rubocop.yml b/.rubocop.yml index 399fc66730..3b4dd79e81 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -26,6 +26,9 @@ Layout/CaseIndentation: Layout/CommentIndentation: Enabled: true +Layout/ElseAlignment: + Enabled: true + Layout/EmptyLineAfterMagicComment: Enabled: true @@ -58,6 +61,9 @@ Layout/IndentationConsistency: Layout/IndentationWidth: Enabled: true +Layout/LeadingCommentSpace: + Enabled: true + Layout/SpaceAfterColon: Enabled: true @@ -73,9 +79,15 @@ Layout/SpaceAroundKeyword: Layout/SpaceAroundOperators: Enabled: true +Layout/SpaceBeforeComma: + Enabled: true + Layout/SpaceBeforeFirstArg: Enabled: true +Style/DefWithParentheses: + Enabled: true + # Defining a method with parameters needs parentheses. Style/MethodDefParentheses: Enabled: true @@ -131,6 +143,7 @@ Style/UnneededPercentQ: Lint/EndAlignment: Enabled: true EnforcedStyleAlignWith: variable + AutoCorrect: true # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. Lint/RequireParentheses: diff --git a/.travis.yml b/.travis.yml index e357a0ca3e..3a7f28134b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: ruby sudo: false cache: - bundler: true directories: + - vendor/bundle - /tmp/cache/unicode_conformance - /tmp/beanstalkd-1.10 - node_modules @@ -11,6 +11,7 @@ cache: services: - memcached + - redis-server addons: postgresql: "9.6" @@ -28,7 +29,7 @@ bundler_args: --without test --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" - "travis_retry gem update --system" - - "travis_retry gem install bundler -v 1.15.4" + - "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_8a915ebdd931_key && -z $encrypted_8a915ebdd931_iv ]] || openssl aes-256-cbc -K $encrypted_8a915ebdd931_key -iv $encrypted_8a915ebdd931_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" @@ -41,7 +42,6 @@ before_script: # Decodes to e.g. `export VARIABLE=VALUE` - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4") - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz") - - redis-server --bind 127.0.0.1 --port 6379 --requirepass 'password' --daemonize yes script: 'ci/travis.rb' @@ -60,59 +60,70 @@ env: - "GEM=ac:integration" rvm: - - 2.2.8 - - 2.3.5 - - 2.4.2 + - 2.2.9 + - 2.3.6 + - 2.4.3 + - 2.5.0 - ruby-head matrix: include: - - rvm: 2.4.2 + - rvm: 2.5.0 env: "GEM=av:ujs" - - rvm: 2.2.8 + - rvm: 2.2.9 env: "GEM=aj:integration" services: - memcached + - redis-server - rabbitmq - - rvm: 2.3.5 + - rvm: 2.3.6 env: "GEM=aj:integration" services: - memcached + - redis-server - rabbitmq - - rvm: 2.4.2 + - rvm: 2.4.3 env: "GEM=aj:integration" services: - memcached + - redis-server + - rabbitmq + - rvm: 2.5.0 + env: "GEM=aj:integration" + services: + - memcached + - redis-server - rabbitmq - rvm: ruby-head env: "GEM=aj:integration" services: - memcached + - redis-server - rabbitmq - - rvm: 2.3.5 + - rvm: 2.3.6 env: - "GEM=ar:mysql2 MYSQL=mariadb" addons: mariadb: 10.2 - - rvm: 2.3.5 + - rvm: 2.3.6 env: - "GEM=ar:sqlite3_mem" - - rvm: 2.3.5 + - rvm: 2.3.6 env: - "GEM=ar:postgresql POSTGRES=9.2" addons: postgresql: "9.2" - - rvm: jruby-9.1.13.0 + - rvm: jruby-9.1.15.0 jdk: oraclejdk8 env: - "GEM=ap" - - rvm: jruby-9.1.13.0 + - rvm: jruby-9.1.15.0 jdk: oraclejdk8 env: - "GEM=am,amo,aj" allow_failures: - rvm: ruby-head - - rvm: jruby-9.1.13.0 + - rvm: jruby-9.1.15.0 - env: "GEM=ac:integration" fast_finish: true @@ -6,8 +6,6 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } gemspec -gem "arel", github: "rails/arel" - # We need a newish Rake since Active Job sets its test tasks' descriptions. gem "rake", ">= 11.1" @@ -19,8 +17,9 @@ gem "capybara", "~> 2.15" gem "rack-cache", "~> 1.2" gem "coffee-rails" -gem "sass-rails", github: "rails/sass-rails", branch: "5-0-stable" +gem "sass-rails" gem "turbolinks", "~> 5" +gem "webmock" # 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) @@ -39,8 +38,11 @@ gem "rubocop", ">= 0.47", require: false # https://github.com/guard/rb-inotify/pull/79 gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false +# https://github.com/puma/puma/pull/1345 +gem "stopgap_13632", platforms: :mri if RUBY_VERSION == "2.2.8" + group :doc do - gem "sdoc", github: "robin850/sdoc", branch: "upgrade" + gem "sdoc", "~> 1.0" gem "redcarpet", "~> 3.2.3", platforms: :ruby gem "w3c_validators" gem "kindlerb", "~> 1.2.0" @@ -50,6 +52,7 @@ end gem "dalli", ">= 2.2.1" gem "listen", ">= 3.0.5", "< 3.2", require: false gem "libxml-ruby", platforms: :ruby +gem "connection_pool" # for railties app_generator_test gem "bootsnap", ">= 1.1.0", require: false @@ -57,15 +60,15 @@ gem "bootsnap", ">= 1.1.0", require: false # Active Job. group :job do gem "resque", require: false - gem "resque-scheduler", github: "jeremy/resque-scheduler", branch: "redis-rb-4.0", require: false + gem "resque-scheduler", require: false gem "sidekiq", require: false gem "sucker_punch", require: false gem "delayed_job", require: false - gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby + gem "queue_classic", github: "rafaelfranca/queue_classic", branch: "update-pg", require: false, platforms: :ruby gem "sneakers", require: false gem "que", require: false gem "backburner", require: false - #TODO: add qu after it support Rails 5.1 + # TODO: add qu after it support Rails 5.1 # gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false # gem "qu-redis", require: false gem "delayed_job_active_record", require: false @@ -76,7 +79,6 @@ end group :cable do gem "puma", require: false - gem "em-hiredis", require: false gem "hiredis", require: false gem "redis", "~> 4.0", require: false @@ -92,7 +94,7 @@ end # Active Storage group :storage do gem "aws-sdk-s3", require: false - gem "google-cloud-storage", "~> 1.3", require: false + gem "google-cloud-storage", "~> 1.8", require: false gem "azure-storage", require: false gem "mini_magick" @@ -119,7 +121,7 @@ group :test do end platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do - gem "nokogiri", ">= 1.6.8" + gem "nokogiri", ">= 1.8.1" # Needed for compiling the ActionDispatch::Journey parser. gem "racc", ">=1.4.6", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 86f6ed3baf..0c9b58febc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,23 +1,4 @@ GIT - remote: https://github.com/QueueClassic/queue_classic.git - revision: cde82d17ded2799ed726dd7b0df6ce1fd4c1b7da - branch: master - specs: - queue_classic (3.2.0.RC1) - pg (>= 0.17, < 0.20) - -GIT - remote: https://github.com/jeremy/resque-scheduler.git - revision: 7cccf7d8db4dbbf54bf549a98b6cbb38106dbed2 - branch: redis-rb-4.0 - specs: - resque-scheduler (4.3.0) - mono_logger (~> 1.0) - redis (>= 3.3, < 5) - resque (~> 1.26) - rufus-scheduler (~> 3.2) - -GIT remote: https://github.com/matthewd/rb-inotify.git revision: 856730aad4b285969e8dd621e44808a7c5af4242 branch: close-handling @@ -35,90 +16,73 @@ GIT websocket GIT - remote: https://github.com/rails/arel.git - revision: 42510bf71472e2e35d9becb546edd05562672344 - specs: - arel (9.0.0.alpha) - -GIT - remote: https://github.com/rails/sass-rails.git - revision: bb5c1d34e8acad2e2960cc785184ffe17d7b3bca - branch: 5-0-stable - specs: - sass-rails (5.0.6) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - -GIT - remote: https://github.com/robin850/sdoc.git - revision: 0e340352f3ab2f196c8a8743f83c2ee286e4f71c - branch: upgrade + remote: https://github.com/rafaelfranca/queue_classic.git + revision: dee64b361355d56700ad7aa3b151bf653a617526 + branch: update-pg specs: - sdoc (1.0.0.rc2) - rdoc (~> 5.0) + queue_classic (3.2.0.RC1) + pg (>= 0.17, < 2.0) PATH remote: . specs: - actioncable (5.2.0.alpha) - actionpack (= 5.2.0.alpha) + actioncable (6.0.0.alpha) + actionpack (= 6.0.0.alpha) nio4r (~> 2.0) - websocket-driver (~> 0.6.1) - actionmailer (5.2.0.alpha) - actionpack (= 5.2.0.alpha) - actionview (= 5.2.0.alpha) - activejob (= 5.2.0.alpha) + websocket-driver (>= 0.6.1) + actionmailer (6.0.0.alpha) + actionpack (= 6.0.0.alpha) + actionview (= 6.0.0.alpha) + activejob (= 6.0.0.alpha) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.0.alpha) - actionview (= 5.2.0.alpha) - activesupport (= 5.2.0.alpha) + actionpack (6.0.0.alpha) + actionview (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.0.alpha) - activesupport (= 5.2.0.alpha) + actionview (6.0.0.alpha) + activesupport (= 6.0.0.alpha) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.0.alpha) - activesupport (= 5.2.0.alpha) + activejob (6.0.0.alpha) + activesupport (= 6.0.0.alpha) globalid (>= 0.3.6) - activemodel (5.2.0.alpha) - activesupport (= 5.2.0.alpha) - activerecord (5.2.0.alpha) - activemodel (= 5.2.0.alpha) - activesupport (= 5.2.0.alpha) - arel (= 9.0.0.alpha) - activestorage (5.2.0.alpha) - actionpack (= 5.2.0.alpha) - activerecord (= 5.2.0.alpha) - activesupport (5.2.0.alpha) + activemodel (6.0.0.alpha) + activesupport (= 6.0.0.alpha) + activerecord (6.0.0.alpha) + activemodel (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) + arel (>= 9.0) + activestorage (6.0.0.alpha) + actionpack (= 6.0.0.alpha) + activerecord (= 6.0.0.alpha) + marcel (~> 0.3.1) + activesupport (6.0.0.alpha) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - rails (5.2.0.alpha) - actioncable (= 5.2.0.alpha) - actionmailer (= 5.2.0.alpha) - actionpack (= 5.2.0.alpha) - actionview (= 5.2.0.alpha) - activejob (= 5.2.0.alpha) - activemodel (= 5.2.0.alpha) - activerecord (= 5.2.0.alpha) - activestorage (= 5.2.0.alpha) - activesupport (= 5.2.0.alpha) + rails (6.0.0.alpha) + actioncable (= 6.0.0.alpha) + actionmailer (= 6.0.0.alpha) + actionpack (= 6.0.0.alpha) + actionview (= 6.0.0.alpha) + activejob (= 6.0.0.alpha) + activemodel (= 6.0.0.alpha) + activerecord (= 6.0.0.alpha) + activestorage (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) bundler (>= 1.3.0) - railties (= 5.2.0.alpha) + railties (= 6.0.0.alpha) sprockets-rails (>= 2.0.0) - railties (5.2.0.alpha) - actionpack (= 5.2.0.alpha) - activesupport (= 5.2.0.alpha) + railties (6.0.0.alpha) + actionpack (= 6.0.0.alpha) + activesupport (= 6.0.0.alpha) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -137,11 +101,12 @@ GEM activerecord-jdbcsqlite3-adapter (1.3.24) activerecord-jdbc-adapter (~> 1.3.24) jdbc-sqlite3 (>= 3.7.2, < 3.9) - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) amq-protocol (2.2.0) archive-zip (0.7.0) io-like (~> 0.3.0) + arel (9.0.0) ast (2.3.0) aws-partitions (1.20.0) aws-sdk-core (3.3.0) @@ -222,37 +187,34 @@ GEM concurrent-ruby (1.0.5-java) connection_pool (2.2.1) cookiejar (0.3.3) + crack (0.4.3) + safe_yaml (~> 1.0.0) + crass (1.0.3) curses (1.0.2) daemons (1.2.4) dalli (2.7.6) dante (0.2.0) - declarative (0.0.9) + declarative (0.0.10) declarative-option (0.1.0) - delayed_job (4.1.3) + delayed_job (4.1.4) activesupport (>= 3.0, < 5.2) delayed_job_active_record (4.1.2) activerecord (>= 3.0, < 5.2) delayed_job (>= 3.0, < 5) digest-crc (0.4.1) - em-hiredis (0.3.1) - eventmachine (~> 1.0) - hiredis (~> 0.6.0) em-http-request (1.1.5) addressable (>= 2.3.4) cookiejar (!= 0.3.1) em-socksify (>= 0.3) eventmachine (>= 1.0.3) http_parser.rb (>= 0.6.0) - em-socksify (0.3.1) + em-socksify (0.3.2) eventmachine (>= 1.0.0.beta.4) - erubi (1.6.1) - et-orbi (1.0.5) + erubi (1.7.0) + et-orbi (1.0.8) tzinfo event_emitter (0.2.6) eventmachine (1.2.5) - eventmachine (1.2.5-java) - eventmachine (1.2.5-x64-mingw32) - eventmachine (1.2.5-x86-mingw32) execjs (2.7.0) faraday (0.13.1) multipart-post (>= 1.2, < 3) @@ -273,37 +235,39 @@ GEM ffi (1.9.18-java) ffi (1.9.18-x64-mingw32) ffi (1.9.18-x86-mingw32) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) - google-api-client (0.13.1) + google-api-client (0.17.3) addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.5) + 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.0.0) + google-cloud-core (1.1.0) google-cloud-env (~> 1.0) - googleauth (~> 0.5.1) google-cloud-env (1.0.1) faraday (~> 0.11) - google-cloud-storage (1.4.0) + google-cloud-storage (1.9.0) digest-crc (~> 0.4) - google-api-client (~> 0.13.0) - google-cloud-core (~> 1.0) - googleauth (0.5.3) + google-api-client (~> 0.17.0) + google-cloud-core (~> 1.1) + googleauth (~> 0.6.2) + googleauth (0.6.2) faraday (~> 0.12) - jwt (~> 1.4) + jwt (>= 1.4, < 3.0) logging (~> 2.0) memoist (~> 0.12) multi_json (~> 1.11) os (~> 0.9) signet (~> 0.7) + hashdiff (0.3.7) hiredis (0.6.1) hiredis (0.6.1-java) http_parser.rb (0.6.0) httpclient (2.8.3) - i18n (0.8.6) + i18n (0.9.3) + concurrent-ruby (~> 1.0) io-like (0.3.0) jdbc-mysql (5.1.44) jdbc-postgres (9.4.1206) @@ -311,7 +275,7 @@ GEM jmespath (1.3.1) json (2.1.0) json (2.1.0-java) - jwt (1.5.6) + jwt (2.1.0) kindlerb (1.2.0) mustache nokogiri @@ -324,24 +288,28 @@ GEM logging (2.2.2) little-plugger (~> 1.1) multi_json (~> 1.10) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) mini_mime (>= 0.1.1) + marcel (0.3.1) + mimemagic (~> 0.3.2) memoist (0.16.0) metaclass (0.0.4) - method_source (0.8.2) + method_source (0.9.0) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) + mimemagic (0.3.2) mini_magick (4.8.0) mini_mime (0.1.4) - mini_portile2 (2.2.0) - minitest (5.10.3) + mini_portile2 (2.3.0) + minitest (5.11.3) minitest-bisect (1.4.0) minitest-server (~> 1.0) path_expander (~> 1.0) - minitest-server (1.0.4) + minitest-server (1.0.5) minitest (~> 5.0) mocha (1.3.0) metaclass (~> 0.0.1) @@ -350,32 +318,33 @@ GEM msgpack (1.1.0-java) msgpack (1.1.0-x64-mingw32) msgpack (1.1.0-x86-mingw32) - multi_json (1.12.1) + multi_json (1.12.2) multipart-post (2.0.0) mustache (1.0.5) mustermann (1.0.0) mysql2 (0.4.9) mysql2 (0.4.9-x64-mingw32) mysql2 (0.4.9-x86-mingw32) - nio4r (2.1.0) - nokogiri (1.8.0) - mini_portile2 (~> 2.2.0) - nokogiri (1.8.0-java) - nokogiri (1.8.0-x64-mingw32) - mini_portile2 (~> 2.2.0) - nokogiri (1.8.0-x86-mingw32) - mini_portile2 (~> 2.2.0) + nio4r (2.2.0) + nio4r (2.2.0-java) + nokogiri (1.8.1) + mini_portile2 (~> 2.3.0) + nokogiri (1.8.1-java) + nokogiri (1.8.1-x64-mingw32) + mini_portile2 (~> 2.3.0) + nokogiri (1.8.1-x86-mingw32) + mini_portile2 (~> 2.3.0) os (0.9.6) parallel (1.12.0) parser (2.4.0.0) ast (~> 2.2) path_expander (1.0.2) - pg (0.19.0) - pg (0.19.0-x64-mingw32) - pg (0.19.0-x86-mingw32) + pg (1.0.0) + pg (1.0.0-x64-mingw32) + pg (1.0.0-x86-mingw32) powerpack (0.1.1) psych (2.2.4) - public_suffix (2.0.5) + public_suffix (3.0.1) puma (3.9.1) puma (3.9.1-java) que (0.14.0) @@ -388,7 +357,7 @@ GEM rack (>= 0.4) rack-protection (2.0.0) rack - rack-test (0.7.0) + rack-test (0.8.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -399,7 +368,7 @@ GEM rake rake (12.2.1) rb-fsevent (0.10.2) - rdoc (5.1.0) + rdoc (6.0.1) redcarpet (3.2.3) redis (4.0.1) redis-namespace (1.6.0) @@ -414,6 +383,11 @@ GEM redis-namespace (~> 1.3) sinatra (>= 0.9.2) vegas (~> 0.1.2) + resque-scheduler (4.3.1) + mono_logger (~> 1.0) + redis (>= 3.3, < 5) + resque (~> 1.26) + rufus-scheduler (~> 3.2) retriable (3.1.1) rubocop (0.51.0) parallel (~> 1.10) @@ -427,11 +401,20 @@ GEM rubyzip (1.2.1) rufus-scheduler (3.4.2) et-orbi (~> 1.0) - sass (3.5.1) + safe_yaml (1.0.4) + sass (3.5.3) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + sass-rails (5.0.7) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + sdoc (1.0.0) + rdoc (>= 5.0) selenium-webdriver (3.5.1) childprocess (~> 0.5) rubyzip (~> 1.0) @@ -444,10 +427,10 @@ GEM rack-protection (>= 1.5.0) redis (>= 3.3.4, < 5) sigdump (0.2.4) - signet (0.7.3) + signet (0.8.1) addressable (~> 2.3) faraday (~> 0.9) - jwt (~> 1.5) + jwt (>= 1.5, < 3.0) multi_json (~> 1.10) sinatra (2.0.0) mustermann (~> 1.0) @@ -500,9 +483,15 @@ GEM json (>= 1.8) nokogiri (~> 1.6) wdm (0.1.1) + webmock (3.2.1) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff websocket (1.2.4) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) + websocket-driver (0.6.5-java) + websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) xpath (2.1.0) nokogiri (~> 1.3) @@ -517,7 +506,6 @@ DEPENDENCIES activerecord-jdbcmysql-adapter (>= 1.3.0) activerecord-jdbcpostgresql-adapter (>= 1.3.0) activerecord-jdbcsqlite3-adapter (>= 1.3.0) - arel! aws-sdk-s3 azure-storage backburner @@ -530,11 +518,11 @@ DEPENDENCIES capybara (~> 2.15) chromedriver-helper coffee-rails + connection_pool dalli (>= 2.2.1) delayed_job delayed_job_active_record - em-hiredis - google-cloud-storage (~> 1.3) + google-cloud-storage (~> 1.8) hiredis json (>= 2.0.0) kindlerb (~> 1.2.0) @@ -544,7 +532,7 @@ DEPENDENCIES minitest-bisect mocha mysql2 (>= 0.4.4) - nokogiri (>= 1.6.8) + nokogiri (>= 1.8.1) pg (>= 0.18.0) psych (~> 2.0) puma @@ -560,10 +548,10 @@ DEPENDENCIES redis (~> 4.0) redis-namespace resque - resque-scheduler! + resque-scheduler rubocop (>= 0.47) - sass-rails! - sdoc! + sass-rails + sdoc (~> 1.0) sequel sidekiq sneakers @@ -576,7 +564,8 @@ DEPENDENCIES uglifier (>= 1.3.0) w3c_validators wdm (>= 0.1.0) + webmock websocket-client-simple! BUNDLED WITH - 1.15.4 + 1.16.1 diff --git a/MIT-LICENSE b/MIT-LICENSE index 6b3cead1a7..8f769c0767 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2017 David Heinemeier Hansson +Copyright (c) 2005-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/RAILS_VERSION b/RAILS_VERSION index d5ebf861d3..747766c587 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -5.2.0.alpha +6.0.0.alpha diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 5b9cc84c09..72f5c5de35 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,27 +1,3 @@ -* Removed deprecated evented redis adapter. - *Rafael Mendonça França* -* Support redis-rb 4.0. - - *Jeremy Daer* - -* Hash long stream identifiers when using PostgreSQL adapter. - - PostgreSQL has a limit on identifiers length (63 chars, [docs](https://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)). - Provided fix minifies identifiers longer than 63 chars by hashing them with SHA1. - - Fixes #28751. - - *Vladimir Dementyev* - -* Action Cable's `redis` adapter allows for other common redis-rb options (`host`, `port`, `db`, `password`) in cable.yml. - - Previously, it accepts only a [redis:// url](https://www.iana.org/assignments/uri-schemes/prov/redis) as an option. - While we can add all of these options to the `url` itself, it is not explicitly documented. This alternative setup - is shown as the first example in the [Redis rubygem](https://github.com/redis/redis-rb#getting-started), which - makes this set of options as sensible as using just the `url`. - - *Marc Rendl Ignacio* - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index 1a0e653b69..a42759f024 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2017 Basecamp, LLC +Copyright (c) 2015-2018 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/README.md b/actioncable/README.md index 44fb81478d..a05ef1dd20 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -558,7 +558,7 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index b5b98f1a6b..51db4dda3a 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -28,5 +28,5 @@ Gem::Specification.new do |s| s.add_dependency "actionpack", version s.add_dependency "nio4r", "~> 2.0" - s.add_dependency "websocket-driver", "~> 0.6.1" + s.add_dependency "websocket-driver", ">= 0.6.1" end diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index 001a043409..e7456e3c1b 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2015-2017 Basecamp, LLC +# Copyright (c) 2015-2018 Basecamp, LLC # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb index 10289ab55c..4b1964c4ae 100644 --- a/actioncable/lib/action_cable/connection/client_socket.rb +++ b/actioncable/lib/action_cable/connection/client_socket.rb @@ -83,7 +83,7 @@ module ActionCable when Numeric then @driver.text(message.to_s) when String then @driver.text(message) when Array then @driver.binary(message) - else false + else false end end diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index af8277d06e..cd1d9bccef 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -7,8 +7,8 @@ module ActionCable end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb index a9c0949950..e384ea4afb 100644 --- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb +++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -gem "pg", "~> 0.18" +gem "pg", ">= 0.18", "< 2.0" require "pg" require "thread" require "digest/sha1" diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt index d672697283..d672697283 100644 --- a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt index 0ff5442f47..0ff5442f47 100644 --- a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt diff --git a/actioncable/lib/rails/generators/channel/templates/assets/cable.js b/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt index 739aa5f022..739aa5f022 100644 --- a/actioncable/lib/rails/generators/channel/templates/assets/cable.js +++ b/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt index 5467811aba..5467811aba 100644 --- a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee +++ b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.js b/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt index ab0e68b11a..ab0e68b11a 100644 --- a/actioncable/lib/rails/generators/channel/templates/assets/channel.js +++ b/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt diff --git a/actioncable/lib/rails/generators/channel/templates/channel.rb b/actioncable/lib/rails/generators/channel/templates/channel.rb.tt index 4bcfb2be4d..4bcfb2be4d 100644 --- a/actioncable/lib/rails/generators/channel/templates/channel.rb +++ b/actioncable/lib/rails/generators/channel/templates/channel.rb.tt diff --git a/actioncable/package.json b/actioncable/package.json index acec1e2e9c..d9df46043d 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -1,6 +1,6 @@ { "name": "actioncable", - "version": "5.2.0-alpha", + "version": "6.0.0-alpha", "description": "WebSocket framework for Ruby on Rails.", "main": "lib/assets/compiled/action_cable.js", "files": [ diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb index 866bd7c21b..3b8eb63975 100644 --- a/actioncable/test/channel/base_test.rb +++ b/actioncable/test/channel/base_test.rb @@ -97,12 +97,12 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase @channel.subscribe_to_channel assert @channel.room - assert @channel.subscribed? + assert_predicate @channel, :subscribed? @channel.unsubscribe_from_channel - assert ! @channel.room - assert ! @channel.subscribed? + assert_not @channel.room + assert_not_predicate @channel, :subscribed? end test "connection identifiers" do diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index eca06fe365..e9e8849637 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -167,7 +167,7 @@ module ActionCable::StreamTests @server.broadcast "channel", {} wait_for_async - refute Thread.current[:ran_callback], "User callback was not run through the worker pool" + assert_not Thread.current[:ran_callback], "User callback was not run through the worker pool" end end @@ -192,10 +192,10 @@ module ActionCable::StreamTests Connection.new(@server, env).tap do |connection| connection.process - assert connection.websocket.possible? + assert_predicate connection.websocket, :possible? wait_for_async - assert connection.websocket.alive? + assert_predicate connection.websocket, :alive? end end diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index 56b3ef143b..ec672a5828 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -307,7 +307,7 @@ class ClientTest < ActionCable::TestCase ActionCable.server.restart c.wait_for_close - assert c.closed? + assert_predicate c, :closed? end end end diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb index 99488e38c8..c69d4d0b36 100644 --- a/actioncable/test/connection/base_test.rb +++ b/actioncable/test/connection/base_test.rb @@ -39,10 +39,10 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase connection = open_connection connection.process - assert connection.websocket.possible? + assert_predicate connection.websocket, :possible? wait_for_async - assert connection.websocket.alive? + assert_predicate connection.websocket, :alive? end end @@ -95,7 +95,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase statistics = connection.statistics - assert statistics[:identifier].blank? + assert_predicate statistics[:identifier], :blank? assert_kind_of Time, statistics[:started_at] assert_equal [], statistics[:subscriptions] end diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb index 2051216010..5c31690c8b 100644 --- a/actioncable/test/connection/client_socket_test.rb +++ b/actioncable/test/connection/client_socket_test.rb @@ -67,9 +67,9 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase env = Rack::MockRequest.env_for "/test", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com" - io = \ + io, client_io = \ begin - Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0).first + Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0) rescue StringIO.new end @@ -77,6 +77,14 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase Connection.new(@server, env).tap do |connection| connection.process + if client_io + # Make sure server returns handshake response + Timeout.timeout(1) do + loop do + break if client_io.readline == "\r\n" + end + end + end connection.send :handle_open assert connection.connected end diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb index 149a40604a..3323785494 100644 --- a/actioncable/test/connection/subscriptions_test.rb +++ b/actioncable/test/connection/subscriptions_test.rb @@ -45,7 +45,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase setup_connection @subscriptions.execute_command "command" => "subscribe" - assert @subscriptions.identifiers.empty? + assert_empty @subscriptions.identifiers end end @@ -58,7 +58,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase channel.expects(:unsubscribe_from_channel) @subscriptions.execute_command "command" => "unsubscribe", "identifier" => @chat_identifier - assert @subscriptions.identifiers.empty? + assert_empty @subscriptions.identifiers end end @@ -67,7 +67,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase setup_connection @subscriptions.execute_command "command" => "unsubscribe" - assert @subscriptions.identifiers.empty? + assert_empty @subscriptions.identifiers end end diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb index c533a9f3eb..b3e9ae9d5c 100644 --- a/actioncable/test/subscription_adapter/common.rb +++ b/actioncable/test/subscription_adapter/common.rb @@ -32,7 +32,7 @@ module CommonSubscriptionAdapterTest subscribed = Concurrent::Event.new adapter.subscribe(channel, callback, Proc.new { subscribed.set }) subscribed.wait(WAIT_WHEN_EXPECTING_EVENT) - assert subscribed.set? + assert_predicate subscribed, :set? yield queue diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 69120d5753..63823d6ef0 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -4,12 +4,15 @@ require "test_helper" require_relative "common" require_relative "channel_prefix" +require "active_support/testing/method_call_assertions" +require "action_cable/subscription_adapter/redis" + class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest include ChannelPrefixTest def cable_config - { adapter: "redis", driver: "ruby", url: "redis://:password@127.0.0.1:6379/12" } + { adapter: "redis", driver: "ruby" } end end @@ -23,6 +26,22 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest def cable_config alt_cable_config = super.dup alt_cable_config.delete(:url) - alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12, password: "password") + alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12) + end +end + +class RedisAdapterTest::Connector < ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + + test "slices url, host, port, db, and password from config" do + config = { url: 1, host: 2, port: 3, db: 4, password: 5 } + + assert_called_with ::Redis, :new, [ config ] do + connect config.merge(other: "unrelated", stuff: "here") + end + end + + def connect(config) + ActionCable::SubscriptionAdapter::Redis.redis_connector.call(config) end end diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 49afec9a12..b2a1c570e5 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,20 +1,3 @@ -* Add `assert_enqueued_email_with` test helper. - assert_enqueued_email_with ContactMailer, :welcome do - ContactMailer.welcome.deliver_later - end - *Mikkel Malmberg* - -* Allow Action Mailer classes to configure their delivery job. - - class MyMailer < ApplicationMailer - self.delivery_job = MyCustomDeliveryJob - - ... - end - - *Matthew Mongeau* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index ac810e86d0..1cb3add0fc 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 9993d3777d..14dfb82234 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -148,7 +148,7 @@ The latest version of Action Mailer can be installed with RubyGems: $ gem install actionmailer -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/actionmailer @@ -166,7 +166,7 @@ API documentation is at * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index ade3fe39a2..fabbdd1b25 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index eb8ae59533..3af95081ee 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -889,7 +889,7 @@ module ActionMailer default_values = self.class.default.map do |key, value| [ key, - value.is_a?(Proc) ? instance_exec(&value) : value + compute_default(value) ] end.to_h @@ -898,6 +898,16 @@ module ActionMailer headers_with_defaults end + def compute_default(value) + return value unless value.is_a?(Proc) + + if value.arity == 1 + instance_exec(self, &value) + else + instance_exec(&value) + end + end + def assign_headers_to_message(message, headers) assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path, :delivery_method, :delivery_method_options) diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index 063d4580d8..72eb5d61e8 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -7,8 +7,8 @@ module ActionMailer end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb index 4bef4a58d3..8a12f805cc 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -4,7 +4,7 @@ require "base64" module ActionMailer # Implements a mailer preview interceptor that converts image tag src attributes - # that use inline cid: style urls to data: style urls so that they are visible + # that use inline cid: style URLs to data: style URLs so that they are visible # when previewing an HTML email in a web browser. # # This interceptor is enabled by default. To disable it, delete it from the diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index a2ea45dc7b..2377aeb9a5 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -53,6 +53,12 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later!(wait: 1.hour) # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now) # + # Options: + # + # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay + # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time + # * <tt>:queue</tt> - Enqueue the email on the specified queue + # # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable # +delivery_job+. @@ -60,12 +66,6 @@ module ActionMailer # class AccountRegistrationMailer < ApplicationMailer # self.delivery_job = RegistrationDeliveryJob # end - # - # Options: - # - # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay - # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time - # * <tt>:queue</tt> - Enqueue the email on the specified queue def deliver_later!(options = {}) enqueue_delivery :deliver_now!, options end @@ -77,6 +77,12 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later(wait: 1.hour) # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now) # + # Options: + # + # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay. + # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time. + # * <tt>:queue</tt> - Enqueue the email on the specified queue. + # # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable # +delivery_job+. @@ -84,12 +90,6 @@ module ActionMailer # class AccountRegistrationMailer < ApplicationMailer # self.delivery_job = RegistrationDeliveryJob # end - # - # Options: - # - # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay. - # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time. - # * <tt>:queue</tt> - Enqueue the email on the specified queue. def deliver_later(options = {}) enqueue_delivery :deliver_now, options end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 730ac89c94..0aea84fd2b 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -104,7 +104,7 @@ module ActionMailer private def load_previews if preview_path - Dir["#{preview_path}/**/*_preview.rb"].each { |file| require_dependency file } + Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file } end end diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt index 00fb9bd48f..00fb9bd48f 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt index 348d314758..348d314758 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 977e0e201e..4124aa00bd 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -506,8 +506,8 @@ class BaseTest < ActiveSupport::TestCase test "should respond to action methods" do assert_respond_to BaseMailer, :welcome assert_respond_to BaseMailer, :implicit_multipart - assert !BaseMailer.respond_to?(:mail) - assert !BaseMailer.respond_to?(:headers) + assert_not_respond_to BaseMailer, :mail + assert_not_respond_to BaseMailer, :headers end test "calling just the action should return the generated mail object" do @@ -725,6 +725,15 @@ class BaseTest < ActiveSupport::TestCase assert(ProcMailer.welcome["x-has-to-proc"].to_s == "symbol") end + test "proc default values can have arity of 1 where arg is a mailer instance" do + assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s, "complex_value") + assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s, "complex_value") + end + + test "proc default values with fixed arity of 0 can be called" do + assert_equal("0", ProcMailer.welcome["X-Lambda-Arity-0"].to_s) + end + test "we can call other defined methods on the class as needed" do mail = ProcMailer.welcome assert_equal("Thanks for signing up this afternoon", mail.subject) diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index ae4e7bf57e..574840c30e 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -105,7 +105,7 @@ class FragmentCachingTest < BaseCachingTest html_safe = @mailer.read_fragment("name") assert_equal content, html_safe - assert html_safe.html_safe? + assert_predicate html_safe, :html_safe? end end @@ -262,7 +262,7 @@ class ViewCacheDependencyTest < BaseCachingTest end def test_view_cache_dependencies_are_empty_by_default - assert NoDependenciesMailer.new.view_cache_dependencies.empty? + assert_empty NoDependenciesMailer.new.view_cache_dependencies end def test_view_cache_dependencies_are_listed_in_declaration_order diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb index b7cf53eb4a..76e730bb79 100644 --- a/actionmailer/test/mailers/proc_mailer.rb +++ b/actionmailer/test/mailers/proc_mailer.rb @@ -4,12 +4,19 @@ class ProcMailer < ActionMailer::Base default to: "system@test.lindsaar.net", "X-Proc-Method" => Proc.new { Time.now.to_i.to_s }, subject: Proc.new { give_a_greeting }, - "x-has-to-proc" => :symbol + "x-has-to-proc" => :symbol, + "X-Lambda-Arity-0" => ->() { "0" }, + "X-Lambda-Arity-1-arg" => ->(arg) { arg.computed_value }, + "X-Lambda-Arity-1-self" => ->(_) { self.computed_value } def welcome mail end + def computed_value + "complex_value" + end + private def give_a_greeting diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index 03e8d4fb66..f8dcb3f4ba 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -39,7 +39,7 @@ class MessageDeliveryTest < ActiveSupport::TestCase end test "its message should be a Mail::Message" do - assert_equal Mail::Message , @mail.message.class + assert_equal Mail::Message, @mail.message.class end test "should respond to .deliver_later" do diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 82035689ad..3c940bc969 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -105,7 +105,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase assert_url_for "/dummy_model", DummyModel # array - assert_url_for "/dummy_model" , [DummyModel] + assert_url_for "/dummy_model", [DummyModel] end def test_signed_up_with_url diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index e01f88e902..0d10a9a3f2 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,139 +1,7 @@ -* Make `assert_recognizes` to traverse mounted engines +* Add alias method `to_hash` to `to_h` for `cookies`. + Add alias method `to_h` to `to_hash` for `session`. - *Yuichiro Kaneko* + *Igor Kasyanchuk* -* Remove deprecated `ActionController::ParamsParser::ParseError`. - *Rafael Mendonça França* - -* Add `:allow_other_host` option to `redirect_back` method. - When `allow_other_host` is set to `false`, the `redirect_back` - will not allow a redirecting from a different host. - `allow_other_host` is `true` by default. - - *Tim Masliuchenko* - -* Add headless chrome support to System Tests. - - *Yuji Yaginuma* - -* Add ability to enable Early Hints for HTTP/2 - - If supported by the server, and enabled in Puma this allows H2 Early Hints to be used. - - The `javascript_include_tag` and the `stylesheet_link_tag` automatically add Early Hints if requested. - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Simplify cookies middleware with key rotation support - - Use the `rotate` method for both `MessageEncryptor` and - `MessageVerifier` to add key rotation support for encrypted and - signed cookies. This also helps simplify support for legacy cookie - security. - - *Michael J Coyne* - -* Use Capybara registered `:puma` server config. - - The Capybara registered `:puma` server ensures the puma server is run in process so - connection sharing and open request detection work correctly by default. - - *Thomas Walpole* - -* Cookies `:expires` option supports `ActiveSupport::Duration` object. - - cookies[:user_name] = { value: "assain", expires: 1.hour } - cookies[:key] = { value: "a yummy cookie", expires: 6.months } - - Pull Request: #30121 - - *Assain Jaleel* - -* Enforce signed/encrypted cookie expiry server side. - - Rails can thwart attacks by malicious clients that don't honor a cookie's expiry. - - It does so by stashing the expiry within the written cookie and relying on the - signing/encrypting to vouch that it hasn't been tampered with. Then on a - server-side read, the expiry is verified and any expired cookie is discarded. - - Pull Request: #30121 - - *Assain Jaleel* - -* Make `take_failed_screenshot` work within engine. - - Fixes #30405. - - *Yuji Yaginuma* - -* Deprecate `ActionDispatch::TestResponse` response aliases - - `#success?`, `#missing?` & `#error?` are not supported by the actual - `ActionDispatch::Response` object and can produce false-positives. Instead, - use the response helpers provided by `Rack::Response`. - - *Trevor Wistaff* - -* Protect from forgery by default - - Rather than protecting from forgery in the generated `ApplicationController`, - add it to `ActionController::Base` depending on - `config.action_controller.default_protect_from_forgery`. This configuration - defaults to false to support older versions which have removed it from their - `ApplicationController`, but is set to true for Rails 5.2. - - *Lisa Ugray* - -* Fallback `ActionController::Parameters#to_s` to `Hash#to_s`. - - *Kir Shatrov* - -* `driven_by` now registers poltergeist and capybara-webkit. - - If poltergeist or capybara-webkit are set as drivers is set for System Tests, - `driven_by` will register the driver and set additional options passed via - the `:options` parameter. - - Refer to the respective driver's documentation to see what options can be passed. - - *Mario Chavez* - -* AEAD encrypted cookies and sessions with GCM. - - Encrypted cookies now use AES-GCM which couples authentication and - encryption in one faster step and produces shorter ciphertexts. Cookies - encrypted using AES in CBC HMAC mode will be seamlessly upgraded when - this new mode is enabled via the - `action_dispatch.use_authenticated_cookie_encryption` configuration value. - - *Michael J Coyne* - -* Change the cache key format for fragments to make it easier to debug key churn. The new format is: - - views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 - ^template path ^template tree digest ^class ^id - - *DHH* - -* Add support for recyclable cache keys with fragment caching. This uses the new versioned entries in the - `ActiveSupport::Cache` stores and relies on the fact that Active Record has split `#cache_key` and `#cache_version` - to support it. - - *DHH* - -* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` - - `ActionController::Base` and `ActionController::API` have differing implementations. This means that - the one umbrella hook `action_controller` is not able to address certain situations where a method - may not exist in a certain implementation. - - This is fixed by adding two new hooks so you can target `ActionController::Base` vs `ActionController::API` - - Fixes #27013. - - *Julian Nadeau* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index ac810e86d0..1cb3add0fc 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 93b2a0932a..f56230ffa0 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -30,7 +30,7 @@ The latest version of Action Pack can be installed with RubyGems: $ gem install actionpack -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/actionpack @@ -44,11 +44,11 @@ Action Pack is released under the MIT license: == Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index bd19b8cd5d..f43784f9f2 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -22,6 +22,7 @@ module ActionController autoload_under "metal" do autoload :ConditionalGet + autoload :ContentSecurityPolicy autoload :Cookies autoload :DataStreaming autoload :EtagWithTemplateDigest diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index b73269871b..204a3d400c 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -225,6 +225,7 @@ module ActionController Flash, FormBuilder, RequestForgeryProtection, + ContentSecurityPolicy, ForceSSL, Streaming, DataStreaming, diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 457884ea08..f875aa5e6b 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -230,18 +230,16 @@ module ActionController # Returns a Rack endpoint for the given action name. def self.action(name) + app = lambda { |env| + req = ActionDispatch::Request.new(env) + res = make_response! req + new.dispatch(name, req, res) + } + if middleware_stack.any? - middleware_stack.build(name) do |env| - req = ActionDispatch::Request.new(env) - res = make_response! req - new.dispatch(name, req, res) - end + middleware_stack.build(name, app) else - lambda { |env| - req = ActionDispatch::Request.new(env) - res = make_response! req - new.dispatch(name, req, res) - } + app end end diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb new file mode 100644 index 0000000000..48a7109bea --- /dev/null +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActionController #:nodoc: + module ContentSecurityPolicy + # TODO: Documentation + extend ActiveSupport::Concern + + module ClassMethods + def content_security_policy(**options, &block) + before_action(options) do + if block_given? + policy = request.content_security_policy.clone + yield policy + request.content_security_policy = policy + end + end + end + + def content_security_policy_report_only(report_only = true, **options) + before_action(options) do + request.content_security_policy_report_only = report_only + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 0ba1f9f783..7de500d119 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -39,7 +39,7 @@ module ActionController # end # # ==== URL Options - # You can pass any of the following options to affect the redirect url + # You can pass any of the following options to affect the redirect URL # * <tt>host</tt> - Redirect to a different host name # * <tt>subdomain</tt> - Redirect to a different subdomain # * <tt>domain</tt> - Redirect to a different domain @@ -73,7 +73,7 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url and + # * <tt>host_or_options</tt> - Either a host name or any of the URL and # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 0c8132684a..01676f3237 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -72,10 +72,10 @@ module ActionController before_action(options.except(:name, :password, :realm)) do authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| # This comparison uses & so that it doesn't short circuit and - # uses `variable_size_secure_compare` so that length information + # uses `secure_compare` so that length information # isn't leaked. - ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & - ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) + ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.secure_compare(password, options[:password]) end end end @@ -350,10 +350,7 @@ module ActionController # authenticate_or_request_with_http_token do |token, options| # # Compare the tokens in a time-constant manner, to mitigate # # timing attacks. - # ActiveSupport::SecurityUtils.secure_compare( - # ::Digest::SHA256.hexdigest(token), - # ::Digest::SHA256.hexdigest(TOKEN) - # ) + # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN) # end # end # end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 8de57f9199..4c2b5120eb 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -68,7 +68,7 @@ module ActionController # if possible, otherwise redirects to the provided default fallback # location. # - # The referrer information is pulled from the HTTP `Referer` (sic) header on + # The referrer information is pulled from the HTTP +Referer+ (sic) header on # the request. This is an optional header and its presence on the request is # subject to browser security settings and user preferences. If the request # is missing this header, the <tt>fallback_location</tt> will be used. @@ -82,8 +82,8 @@ module ActionController # redirect_back fallback_location: '/', allow_other_host: false # # ==== Options - # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing `Referer` header. - # * <tt>:allow_other_host</tt> - Allows or disallow redirection to the host that is different to the current host + # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header. + # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true. # # All other options that can be passed to <tt>redirect_to</tt> are accepted as # options and the behavior is identical. diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bd133f24a1..0ab313e398 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -3,6 +3,7 @@ require "rack/session/abstract/id" require "action_controller/metal/exceptions" require "active_support/security_utils" +require "active_support/core_ext/string/strip" module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -216,7 +217,7 @@ module ActionController #:nodoc: # The actual before_action that is used to verify the CSRF token. # Don't override this directly. Provide your own forgery protection # strategy instead. If you override, you'll disable same-origin - # `<script>` verification. + # <tt><script></tt> verification. # # Lean on the protect_from_forgery declaration to mark which actions are # due for same-origin request verification. If protect_from_forgery is @@ -250,7 +251,7 @@ module ActionController #:nodoc: private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING # :startdoc: - # If `verify_authenticity_token` was run (indicating that we have + # If +verify_authenticity_token+ was run (indicating that we have # forgery protection enabled for this request) then also verify that # we aren't serving an unauthorized cross-origin response. def verify_same_origin_request # :doc: @@ -267,7 +268,7 @@ module ActionController #:nodoc: @marked_for_same_origin_verification = request.get? end - # If the `verify_authenticity_token` before_action ran, verify that + # If the +verify_authenticity_token+ before_action ran, verify that # JavaScript responses are only served to same-origin GET requests. def marked_for_same_origin_verification? # :doc: @marked_for_same_origin_verification ||= false @@ -369,7 +370,7 @@ module ActionController #:nodoc: end def compare_with_real_token(token, session) # :doc: - ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session)) end def valid_per_form_csrf_token?(token, session) # :doc: @@ -380,7 +381,7 @@ module ActionController #:nodoc: request.request_method ) - ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token) else false end @@ -415,11 +416,21 @@ module ActionController #:nodoc: allow_forgery_protection end + NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc + The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually + means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the + best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. + If you cannot change the referrer policy, you can disable origin checking with the + Rails.application.config.action_controller.forgery_protection_origin_check setting. + MSG + # Checks if the request originated from the same origin by looking at the # Origin header. def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. + raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 0b1598bf1b..8dc01a5eb9 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -183,7 +183,7 @@ module ActionController #:nodoc: # unicorn_rails --config-file unicorn.config.rb # # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. - # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen + # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen # # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index ef7c4c4c16..a56ac749f8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -335,7 +335,7 @@ module ActionController # the same way as <tt>Hash#each_pair</tt>. def each_pair(&block) @parameters.each_pair do |key, value| - yield key, convert_hashes_to_parameters(key, value) + yield [key, convert_hashes_to_parameters(key, value)] end end alias_method :each, :each_pair diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 4b408750a4..798d142755 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -256,7 +256,7 @@ module ActionController # # def test_create # json = {book: { title: "Love Hina" }}.to_json - # post :create, json + # post :create, body: json # end # # == Special instance variables diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 34937f3229..0822cdc0a6 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -42,6 +42,7 @@ module ActionDispatch eager_autoload do autoload_under "http" do + autoload :ContentSecurityPolicy autoload :Request autoload :Response end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 3328ce17a0..a8febc32b3 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -133,7 +133,7 @@ module ActionDispatch end def generate_strong_etag(validators) - %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") + %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") end def cache_control_segments diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb new file mode 100644 index 0000000000..4883e23d24 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +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 + + def initialize(app) + @app = app + end + + def call(env) + request = ActionDispatch::Request.new env + _, headers, _ = response = @app.call(env) + + return response unless html_response?(headers) + return response if policy_present?(headers) + + if policy = request.content_security_policy + headers[header_name(request)] = policy.build(request.controller_instance) + end + + response + end + + private + + def html_response?(headers) + if content_type = headers[CONTENT_TYPE] + content_type =~ /html/ + end + end + + def header_name(request) + if request.content_security_policy_report_only + POLICY_REPORT_ONLY + else + POLICY + end + end + + def policy_present?(headers) + headers[POLICY] || headers[POLICY_REPORT_ONLY] + end + end + + module Request + POLICY = "action_dispatch.content_security_policy".freeze + POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze + + def content_security_policy + get_header(POLICY) + end + + def content_security_policy=(policy) + set_header(POLICY, policy) + end + + def content_security_policy_report_only + get_header(POLICY_REPORT_ONLY) + end + + def content_security_policy_report_only=(value) + set_header(POLICY_REPORT_ONLY, value) + end + end + + MAPPINGS = { + self: "'self'", + unsafe_eval: "'unsafe-eval'", + unsafe_inline: "'unsafe-inline'", + none: "'none'", + http: "http:", + https: "https:", + data: "data:", + mediastream: "mediastream:", + blob: "blob:", + filesystem: "filesystem:", + report_sample: "'report-sample'", + strict_dynamic: "'strict-dynamic'" + }.freeze + + DIRECTIVES = { + base_uri: "base-uri", + child_src: "child-src", + connect_src: "connect-src", + default_src: "default-src", + font_src: "font-src", + form_action: "form-action", + frame_ancestors: "frame-ancestors", + frame_src: "frame-src", + img_src: "img-src", + manifest_src: "manifest-src", + media_src: "media-src", + object_src: "object-src", + script_src: "script-src", + style_src: "style-src", + worker_src: "worker-src" + }.freeze + + private_constant :MAPPINGS, :DIRECTIVES + + attr_reader :directives + + def initialize + @directives = {} + yield self if block_given? + end + + def initialize_copy(other) + @directives = other.directives.deep_dup + end + + DIRECTIVES.each do |name, directive| + define_method(name) do |*sources| + if sources.first + @directives[directive] = apply_mappings(sources) + else + @directives.delete(directive) + end + end + end + + def block_all_mixed_content(enabled = true) + if enabled + @directives["block-all-mixed-content"] = true + else + @directives.delete("block-all-mixed-content") + end + end + + def plugin_types(*types) + if types.first + @directives["plugin-types"] = types + else + @directives.delete("plugin-types") + end + end + + def report_uri(uri) + @directives["report-uri"] = [uri] + end + + def require_sri_for(*types) + if types.first + @directives["require-sri-for"] = types + else + @directives.delete("require-sri-for") + end + end + + def sandbox(*values) + if values.empty? + @directives["sandbox"] = true + elsif values.first + @directives["sandbox"] = values + else + @directives.delete("sandbox") + end + end + + def upgrade_insecure_requests(enabled = true) + if enabled + @directives["upgrade-insecure-requests"] = true + else + @directives.delete("upgrade-insecure-requests") + end + end + + def build(context = nil) + build_directives(context).compact.join("; ") + ";" + end + + private + def apply_mappings(sources) + sources.map do |source| + case source + when Symbol + apply_mapping(source) + when String, Proc + source + else + raise ArgumentError, "Invalid content security policy source: #{source.inspect}" + end + end + end + + def apply_mapping(source) + MAPPINGS.fetch(source) do + raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}" + end + end + + def build_directives(context) + @directives.map do |directive, sources| + if sources.is_a?(Array) + "#{directive} #{build_directive(sources, context).join(' ')}" + elsif sources + directive + else + nil + end + end + end + + def build_directive(sources, context) + sources.map { |source| resolve_source(source, context) } + end + + def resolve_source(source, context) + case source + when String + source + when Symbol + source.to_s + when Proc + if context.nil? + raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}" + else + context.instance_exec(&source) + end + else + raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" + end + end + end +end diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index f8e6fca36d..342e6de312 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -10,6 +10,7 @@ Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv Mime::Type.register "text/vcard", :vcf +Mime::Type.register "text/vtt", :vtt, %w(vtt) Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) @@ -20,6 +21,18 @@ Mime::Type.register "image/svg+xml", :svg Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) +Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3) +Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus) +Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac) + +Mime::Type.register "video/webm", :webm, [], %w(webm) +Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v) + +Mime::Type.register "font/otf", :otf, [], %w(otf) +Mime::Type.register "font/ttf", :ttf, [], %w(ttf) +Mime::Type.register "font/woff", :woff, [], %w(woff) +Mime::Type.register "font/woff2", :woff2, [], %w(woff2) + Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index d631281e4b..3838b84a7a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -22,6 +22,7 @@ module ActionDispatch include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL + include ActionDispatch::ContentSecurityPolicy::Request include Rack::Request::Env autoload :Session, "action_dispatch/request/session" diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f0344fd927..35ba44005a 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -274,7 +274,7 @@ module ActionDispatch def standard_port case protocol when "https://" then 443 - else 80 + else 80 end end diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index bdb78d8d48..56e9e3c83d 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -9,16 +9,16 @@ module ActionDispatch " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];" } - #memo_nodes = memos.values.flatten.map { |n| - # label = n - # if Journey::Route === n - # label = "#{n.verb.source} #{n.path.spec}" - # end - # " #{n.object_id} [label=\"#{label}\", shape=box];" - #} - #memo_edges = memos.flat_map { |k, memos| - # (memos || []).map { |v| " #{k} -> #{v.object_id};" } - #}.uniq + # memo_nodes = memos.values.flatten.map { |n| + # label = n + # if Journey::Route === n + # label = "#{n.verb.source} #{n.path.spec}" + # end + # " #{n.object_id} [label=\"#{label}\", shape=box];" + # } + # memo_edges = memos.flat_map { |k, memos| + # (memos || []).map { |v| " #{k} -> #{v.object_id};" } + # }.uniq <<-eodot digraph nfa { diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 86a070c6ad..c45d947904 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -161,7 +161,7 @@ module ActionDispatch # # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. - # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2. # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. @@ -338,6 +338,9 @@ module ActionDispatch end alias :has_key? :key? + # Returns the cookies as Hash. + alias :to_hash :to_h + def update(other_hash) @cookies.update other_hash.stringify_keys self diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 4f69abfa6f..d1b4508378 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -25,6 +25,7 @@ module ActionDispatch "ActionView::MissingTemplate" => "missing_template", "ActionController::RoutingError" => "routing_error", "AbstractController::ActionNotFound" => "unknown_action", + "ActiveRecord::StatementInvalid" => "invalid_statement", "ActionView::Template::Error" => "template_error" ) diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 805d3f2148..da2871b551 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -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\-@]/, "".freeze).first(255) else internal_request_id end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb new file mode 100644 index 0000000000..e1b129ccc5 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb @@ -0,0 +1,21 @@ +<header> + <h1> + <%= @exception.class.to_s %> + <% if @request.parameters['controller'] %> + in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> + <% end %> + </h1> +</header> + +<div id="container"> + <h2> + <%= h @exception.message %> + <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %> + <br />To resolve this issue run: bin/rails active_storage:install + <% end %> + </h2> + + <%= render template: "rescues/_source" %> + <%= render template: "rescues/_trace" %> + <%= render template: "rescues/_request_and_response" %> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb new file mode 100644 index 0000000000..033518cf8a --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb @@ -0,0 +1,13 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %> +To resolve this issue run: bin/rails active_storage:install +<% end %> + +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 855f2ffa47..eb6fbca6ba 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -26,7 +26,10 @@ module ActionDispatch config.action_dispatch.default_headers = { "X-Frame-Options" => "SAMEORIGIN", "X-XSS-Protection" => "1; mode=block", - "X-Content-Type-Options" => "nosniff" + "X-Content-Type-Options" => "nosniff", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" } config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index d86d0b10c2..000847e193 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -130,6 +130,7 @@ module ActionDispatch load_for_read! @delegate.dup.delete_if { |_, v| v.nil? } end + alias :to_h :to_hash # Updates the session with given Hash. # diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ded42adee9..31eb6104fe 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1573,7 +1573,7 @@ module ActionDispatch # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # - # match 'path' => 'controller#action', via: patch + # match 'path' => 'controller#action', via: :patch # match 'path', to: 'controller#action', via: :post # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest, &block) @@ -2046,7 +2046,7 @@ module ActionDispatch end module CustomUrls - # Define custom url helpers that will be added to the application's + # Define custom URL helpers that will be added to the application's # routes. This allows you to override and/or replace the default behavior # of routing helpers, e.g: # @@ -2066,11 +2066,11 @@ module ActionDispatch # arguments for +url_for+ which will actually build the URL string. This can # be one of the following: # - # * A string, which is treated as a generated URL - # * A hash, e.g. { controller: "pages", action: "index" } - # * An array, which is passed to `polymorphic_url` - # * An Active Model instance - # * An Active Model class + # * A string, which is treated as a generated URL + # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt> + # * An array, which is passed to +polymorphic_url+ + # * An Active Model instance + # * An Active Model class # # NOTE: Other URL helpers can be called in the block but be careful not to invoke # your custom URL helper again otherwise it will result in a stack overflow error. @@ -2082,9 +2082,9 @@ module ActionDispatch # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ] # end # - # In this instance the +params+ object comes from the context in which the the + # In this instance the +params+ object comes from the context in which the # block is executed, e.g. generating a URL inside a controller action or a view. - # If the block is executed where there isn't a params object such as this: + # If the block is executed where there isn't a +params+ object such as this: # # Rails.application.routes.url_helpers.browse_path # diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 987e709f6f..ff6998ae31 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -2,7 +2,6 @@ require "action_dispatch/journey" require "active_support/core_ext/object/to_query" -require "active_support/core_ext/hash/slice" require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/array/extract_options" @@ -199,6 +198,16 @@ module ActionDispatch if args.size == arg_size && !inner_options && optimize_routes_generation?(t) options = t.url_options.merge @options options[:path] = optimized_helper(args) + + original_script_name = options.delete(:original_script_name) + script_name = t._routes.find_script_name(options) + + if original_script_name + script_name = original_script_name + script_name + end + + options[:script_name] = script_name + url_strategy.call options else super @@ -845,7 +854,7 @@ module ActionDispatch recognize_path_with_request(req, path, extras) end - def recognize_path_with_request(req, path, extras) + def recognize_path_with_request(req, path, extras, raise_on_missing: true) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -865,12 +874,14 @@ module ActionDispatch return req.path_parameters elsif app.matches?(req) && app.engine? - path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras) - return path_parameters + path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false) + return path_parameters if path_parameters end end - raise ActionController::RoutingError, "No route matches #{path.inspect}" + if raise_on_missing + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end end end # :startdoc: diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 3ae533dd37..fa345dccdf 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -155,7 +155,7 @@ module ActionDispatch # Missing routes keys may be filled in from the current request's parameters # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are # placed in the path). Given that the current action has been reached - # through `GET /users/1`: + # through <tt>GET /users/1</tt>: # # url_for(only_path: true) # => '/users/1' # url_for(only_path: true, action: 'edit') # => '/users/1/edit' diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index 7246e01cff..f85f816bb9 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -6,6 +6,7 @@ require "capybara/dsl" require "capybara/minitest" require "action_controller" require "action_dispatch/system_testing/driver" +require "action_dispatch/system_testing/browser" require "action_dispatch/system_testing/server" require "action_dispatch/system_testing/test_helpers/screenshot_helper" require "action_dispatch/system_testing/test_helpers/setup_and_teardown" @@ -69,6 +70,9 @@ module ActionDispatch # size of the browser screen. These two options are not applicable for # headless drivers and will be silently ignored if passed. # + # Headless browsers such as headless Chrome and headless Firefox are also supported. + # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+. + # # To use a headless driver, like Poltergeist, update your Gemfile to use # Poltergeist instead of Selenium and then declare the driver name in the # +application_system_test_case.rb+ file. In this case, you would leave out @@ -121,11 +125,15 @@ module ActionDispatch # # driven_by :poltergeist # - # driven_by :selenium, using: :firefox + # driven_by :selenium, screen_size: [800, 800] + # + # driven_by :selenium, using: :chrome # # driven_by :selenium, using: :headless_chrome # - # driven_by :selenium, screen_size: [800, 800] + # driven_by :selenium, using: :firefox + # + # driven_by :selenium, using: :headless_firefox def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}) self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options) end diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb new file mode 100644 index 0000000000..10e6888ab3 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ActionDispatch + module SystemTesting + class Browser # :nodoc: + attr_reader :name + + def initialize(name) + @name = name + end + + def type + case name + when :headless_chrome + :chrome + when :headless_firefox + :firefox + else + name + end + end + + def options + case name + when :headless_chrome + headless_chrome_browser_options + when :headless_firefox + headless_firefox_browser_options + end + end + + private + def headless_chrome_browser_options + options = Selenium::WebDriver::Chrome::Options.new + options.args << "--headless" + options.args << "--disable-gpu" + + options + end + + def headless_firefox_browser_options + options = Selenium::WebDriver::Firefox::Options.new + options.args << "-headless" + + options + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 2687772b4b..5252ff6746 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -5,7 +5,7 @@ module ActionDispatch class Driver # :nodoc: def initialize(name, **options) @name = name - @browser = options[:using] + @browser = Browser.new(options[:using]) @screen_size = options[:screen_size] @options = options[:options] end @@ -32,23 +32,11 @@ module ActionDispatch end def browser_options - if @browser == :headless_chrome - browser_options = Selenium::WebDriver::Chrome::Options.new - browser_options.args << "--headless" - browser_options.args << "--disable-gpu" - - @options.merge(options: browser_options) - else - @options - end - end - - def browser - @browser == :headless_chrome ? :chrome : @browser + @options.merge(options: @browser.options).compact end def register_selenium(app) - Capybara::Selenium::Driver.new(app, { browser: browser }.merge(browser_options)).tap do |driver| + Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver| driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) end end diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb index 8f1b6725b1..4fc1f33767 100644 --- a/actionpack/lib/action_dispatch/system_testing/server.rb +++ b/actionpack/lib/action_dispatch/system_testing/server.rb @@ -20,7 +20,7 @@ module ActionDispatch end def set_server - Capybara.server = :puma, { Silent: self.class.silence_puma } + Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default] end def set_port 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 6c337cdc31..df0c5d3f0e 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 @@ -15,12 +15,11 @@ module ActionDispatch # # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to # control the output. Possible values are: - # * [+inline+ (default)] display the screenshot in the terminal using the + # * [+simple+ (default)] Only displays the screenshot path. + # This is the default value. + # * [+inline+] Display the screenshot in the terminal using the # iTerm image protocol (https://iterm2.com/documentation-images.html). - # * [+simple+] only display the screenshot path. - # This is the default value if the +CI+ environment variables - # is defined. - # * [+artifact+] display the screenshot in the terminal, using the terminal + # * [+artifact+] Display the screenshot in the terminal, using the terminal # artifact format (https://buildkite.github.io/terminal/inline-images/). def take_screenshot save_image @@ -59,11 +58,8 @@ module ActionDispatch # Environment variables have priority output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"] - # If running in a CI environment, default to simple - output_type ||= "simple" if ENV["CI"] - - # Default - output_type ||= "inline" + # Default to outputting a path to the screenshot + output_type ||= "simple" output_type end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 95fdd3affb..3f69109633 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 28bc153f4d..37969fcb57 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -7,8 +7,8 @@ module ActionPack end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 5262e85a28..f4787ed27a 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -380,10 +380,8 @@ class ForkingExecutor def initialize(size) @size = size @queue = Server.new - file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname("rails-tests", "fd") - @url = "drbunix://#{file}" @pool = nil - DRb.start_service @url, @queue + @url = DRb.start_service("drbunix:", @queue).uri end def <<(work); @queue << work; end @@ -453,3 +451,7 @@ end class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase driven_by :selenium, using: :headless_chrome end + +class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_firefox +end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index f9a037e3cc..504c77b8ef 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -301,18 +301,18 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_empty_flash process :flash_me_naked - assert flash.empty? + assert_empty flash end def test_flash_exist process :flash_me - assert flash.any? - assert flash["hello"].present? + assert_predicate flash, :any? + assert_predicate flash["hello"], :present? end def test_flash_does_not_exist process :nothing - assert flash.empty? + assert_empty flash end def test_session_exist @@ -322,7 +322,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def session_does_not_exist process :nothing - assert session.empty? + assert_empty session end def test_redirection_location @@ -343,46 +343,46 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_server_error_response_code process :response500 - assert @response.server_error? + assert_predicate @response, :server_error? process :response599 - assert @response.server_error? + assert_predicate @response, :server_error? process :response404 - assert !@response.server_error? + assert_not_predicate @response, :server_error? end def test_missing_response_code process :response404 - assert @response.not_found? + assert_predicate @response, :not_found? end def test_client_error_response_code process :response404 - assert @response.client_error? + assert_predicate @response, :client_error? end def test_redirect_url_match process :redirect_external - assert @response.redirect? + assert_predicate @response, :redirect? assert_match(/rubyonrails/, @response.redirect_url) assert !/perloffrails/.match(@response.redirect_url) end def test_redirection process :redirect_internal - assert @response.redirect? + assert_predicate @response, :redirect? process :redirect_external - assert @response.redirect? + assert_predicate @response, :redirect? process :nothing - assert !@response.redirect? + assert_not_predicate @response, :redirect? end def test_successful_response_code process :nothing - assert @response.successful? + assert_predicate @response, :successful? end def test_response_object diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb index fd1997f26c..e366ce9532 100644 --- a/actionpack/test/controller/api/conditional_get_test.rb +++ b/actionpack/test/controller/api/conditional_get_test.rb @@ -53,7 +53,7 @@ class ConditionalGetApiTest < ActionController::TestCase @request.if_modified_since = @last_modified get :one assert_equal 304, @response.status.to_i - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal @last_modified, @response.headers["Last-Modified"] end end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 9ac82c0d65..a672ede1a9 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -107,9 +107,9 @@ class ControllerInstanceTests < ActiveSupport::TestCase end def test_performed? - assert !@empty.performed? + assert_not_predicate @empty, :performed? @empty.response_body = ["sweet"] - assert @empty.performed? + assert_predicate @empty, :performed? end def test_action_methods diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 3557f9f888..8b596083d5 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -159,7 +159,7 @@ class FragmentCachingTest < ActionController::TestCase html_safe = @controller.read_fragment("name") assert_equal content, html_safe - assert html_safe.html_safe? + assert_predicate html_safe, :html_safe? end end @@ -382,7 +382,7 @@ class ViewCacheDependencyTest < ActionController::TestCase end def test_view_cache_dependencies_are_empty_by_default - assert NoDependenciesController.new.view_cache_dependencies.empty? + assert_empty NoDependenciesController.new.view_cache_dependencies end def test_view_cache_dependencies_are_listed_in_declaration_order diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 9f0a9dec7a..2b16a555bb 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -819,7 +819,7 @@ class FilterTest < ActionController::TestCase response = test_process(RescuedController) end - assert response.successful? + assert_predicate response, :successful? assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) end diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index f31a4d9329..6c3ac26de1 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -92,11 +92,11 @@ module ActionDispatch end def test_empty? - assert @hash.empty? + assert_empty @hash @hash["zomg"] = "bears" - assert !@hash.empty? + assert_not_empty @hash @hash.clear - assert @hash.empty? + assert_empty @hash end def test_each diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index fd1c5e693f..a685f5868e 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -14,11 +14,11 @@ class SessionTest < ActiveSupport::TestCase end def test_https_bang_works_and_sets_truth_by_default - assert !@session.https? + assert_not_predicate @session, :https? @session.https! - assert @session.https? + assert_predicate @session, :https? @session.https! false - assert !@session.https? + assert_not_predicate @session, :https? end def test_host! @@ -412,11 +412,11 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest get "/get_with_params", params: { foo: "bar" } - assert request.env["rack.input"].string.empty? + assert_empty request.env["rack.input"].string assert_equal "foo=bar", request.env["QUERY_STRING"] assert_equal "foo=bar", request.query_string assert_equal "bar", request.parameters["foo"] - assert request.parameters["leaks"].nil? + assert_predicate request.parameters["leaks"], :nil? end end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 8cfb43a6bc..431fe90b23 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -464,7 +464,7 @@ module ActionController end def test_stale_with_etag - @request.if_none_match = %(W/"#{Digest::MD5.hexdigest('123')}") + @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}") get :with_stale assert_equal 304, response.status.to_i end diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb index c235c9df86..248ef36b7c 100644 --- a/actionpack/test/controller/metal_test.rb +++ b/actionpack/test/controller/metal_test.rb @@ -9,7 +9,7 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase end end - def test_response_has_default_headers + def test_response_does_not_have_default_headers original_default_headers = ActionDispatch::Response.default_headers ActionDispatch::Response.default_headers = { @@ -23,9 +23,9 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase "rack.input" => -> {} )[1] - refute response_headers.key?("X-Frame-Options") - refute response_headers.key?("X-Content-Type-Options") - refute response_headers.key?("X-XSS-Protection") + assert_not response_headers.key?("X-Frame-Options") + assert_not response_headers.key?("X-Content-Type-Options") + assert_not response_headers.key?("X-XSS-Protection") ensure ActionDispatch::Response.default_headers = original_default_headers end diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb index e33a99068f..d683bc73e6 100644 --- a/actionpack/test/controller/output_escaping_test.rb +++ b/actionpack/test/controller/output_escaping_test.rb @@ -4,7 +4,7 @@ require "abstract_unit" class OutputEscapingTest < ActiveSupport::TestCase test "escape_html shouldn't die when passed nil" do - assert ERB::Util.h(nil).blank? + assert_predicate ERB::Util.h(nil), :blank? end test "escapeHTML should escape strings" do diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 43cabae7d2..7bcf8c380d 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -22,13 +22,13 @@ class ParametersAccessorsTest < ActiveSupport::TestCase test "[] retains permitted status" do @params.permit! - assert @params[:person].permitted? - assert @params[:person][:name].permitted? + assert_predicate @params[:person], :permitted? + assert_predicate @params[:person][:name], :permitted? end test "[] retains unpermitted status" do - assert_not @params[:person].permitted? - assert_not @params[:person][:name].permitted? + assert_not_predicate @params[:person], :permitted? + assert_not_predicate @params[:person][:name], :permitted? end test "as_json returns the JSON representation of the parameters hash" do @@ -51,6 +51,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each { |key, value| assert_not(value.permitted?) if key == "person" } end + test "each returns key,value array for block with arity 1" do + @params.each do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + test "each_pair carries permitted status" do @params.permit! @params.each_pair { |key, value| assert(value.permitted?) if key == "person" } @@ -60,35 +68,43 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" } end + test "each_pair returns key,value array for block with arity 1" do + @params.each_pair do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + test "empty? returns true when params contains no key/value pairs" do params = ActionController::Parameters.new - assert params.empty? + assert_empty params end test "empty? returns false when any params are present" do - refute @params.empty? + assert_not_empty @params end test "except retains permitted status" do @params.permit! - assert @params.except(:person).permitted? - assert @params[:person].except(:name).permitted? + assert_predicate @params.except(:person), :permitted? + assert_predicate @params[:person].except(:name), :permitted? end test "except retains unpermitted status" do - assert_not @params.except(:person).permitted? - assert_not @params[:person].except(:name).permitted? + assert_not_predicate @params.except(:person), :permitted? + assert_not_predicate @params[:person].except(:name), :permitted? end test "fetch retains permitted status" do @params.permit! - assert @params.fetch(:person).permitted? - assert @params[:person].fetch(:name).permitted? + assert_predicate @params.fetch(:person), :permitted? + assert_predicate @params[:person].fetch(:name), :permitted? end test "fetch retains unpermitted status" do - assert_not @params.fetch(:person).permitted? - assert_not @params[:person].fetch(:name).permitted? + assert_not_predicate @params.fetch(:person), :permitted? + assert_not_predicate @params[:person].fetch(:name), :permitted? end test "has_key? returns true if the given key is present in the params" do @@ -96,7 +112,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase end test "has_key? returns false if the given key is not present in the params" do - refute @params.has_key?(:address) + assert_not @params.has_key?(:address) end test "has_value? returns true if the given value is present in the params" do @@ -106,7 +122,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase test "has_value? returns false if the given value is not present in the params" do params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") - refute params.has_value?("New York") + assert_not params.has_value?("New York") end test "include? returns true if the given key is present in the params" do @@ -114,7 +130,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase end test "include? returns false if the given key is not present in the params" do - refute @params.include?(:address) + assert_not @params.include?(:address) end test "key? returns true if the given key is present in the params" do @@ -122,7 +138,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase end test "key? returns false if the given key is not present in the params" do - refute @params.key?(:address) + assert_not @params.key?(:address) end test "keys returns an array of the keys of the params" do @@ -131,48 +147,48 @@ class ParametersAccessorsTest < ActiveSupport::TestCase end test "reject retains permitted status" do - assert_not @params.reject { |k| k == "person" }.permitted? + assert_not_predicate @params.reject { |k| k == "person" }, :permitted? end test "reject retains unpermitted status" do @params.permit! - assert @params.reject { |k| k == "person" }.permitted? + assert_predicate @params.reject { |k| k == "person" }, :permitted? end test "select retains permitted status" do @params.permit! - assert @params.select { |k| k == "person" }.permitted? + assert_predicate @params.select { |k| k == "person" }, :permitted? end test "select retains unpermitted status" do - assert_not @params.select { |k| k == "person" }.permitted? + assert_not_predicate @params.select { |k| k == "person" }, :permitted? end test "slice retains permitted status" do @params.permit! - assert @params.slice(:person).permitted? + assert_predicate @params.slice(:person), :permitted? end test "slice retains unpermitted status" do - assert_not @params.slice(:person).permitted? + assert_not_predicate @params.slice(:person), :permitted? end test "transform_keys retains permitted status" do @params.permit! - assert @params.transform_keys { |k| k }.permitted? + assert_predicate @params.transform_keys { |k| k }, :permitted? end test "transform_keys retains unpermitted status" do - assert_not @params.transform_keys { |k| k }.permitted? + assert_not_predicate @params.transform_keys { |k| k }, :permitted? end test "transform_values retains permitted status" do @params.permit! - assert @params.transform_values { |v| v }.permitted? + assert_predicate @params.transform_values { |v| v }, :permitted? end test "transform_values retains unpermitted status" do - assert_not @params.transform_values { |v| v }.permitted? + assert_not_predicate @params.transform_values { |v| v }, :permitted? end test "value? returns true if the given value is present in the params" do @@ -182,7 +198,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase test "value? returns false if the given value is not present in the params" do params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") - refute params.value?("New York") + assert_not params.value?("New York") end test "values returns an array of the values of the params" do @@ -192,13 +208,13 @@ class ParametersAccessorsTest < ActiveSupport::TestCase test "values_at retains permitted status" do @params.permit! - assert @params.values_at(:person).first.permitted? - assert @params[:person].values_at(:name).first.permitted? + assert_predicate @params.values_at(:person).first, :permitted? + assert_predicate @params[:person].values_at(:name).first, :permitted? end test "values_at retains unpermitted status" do - assert_not @params.values_at(:person).first.permitted? - assert_not @params[:person].values_at(:name).first.permitted? + assert_not_predicate @params.values_at(:person).first, :permitted? + assert_not_predicate @params[:person].values_at(:name).first, :permitted? end test "is equal to Parameters instance with same params" do @@ -273,7 +289,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase else test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do assert_not ActionController::Parameters.method_defined?(:dig) - assert_not @params.respond_to?(:dig) + assert_not_respond_to @params, :dig end end end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index 1e8b71d789..fe0e5e368d 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -25,6 +25,6 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase book: { pages: 65 }, format: "json") permitted = params.permit book: [:pages] - assert permitted.permitted? + assert_predicate permitted, :permitted? end end diff --git a/actionpack/test/controller/parameters/dup_test.rb b/actionpack/test/controller/parameters/dup_test.rb index f5833aff46..5403fc6d93 100644 --- a/actionpack/test/controller/parameters/dup_test.rb +++ b/actionpack/test/controller/parameters/dup_test.rb @@ -23,7 +23,7 @@ class ParametersDupTest < ActiveSupport::TestCase test "a duplicate maintains the original's permitted status" do @params.permit! dupped_params = @params.dup - assert dupped_params.permitted? + assert_predicate dupped_params, :permitted? end test "a duplicate maintains the original's parameters" do @@ -57,11 +57,11 @@ class ParametersDupTest < ActiveSupport::TestCase dupped_params = @params.deep_dup dupped_params.permit! - assert_not @params.permitted? + assert_not_predicate @params, :permitted? end test "deep_dup @permitted is being copied" do @params.permit! - assert @params.deep_dup.permitted? + assert_predicate @params.deep_dup, :permitted? end end diff --git a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb index dcf848a620..c890839727 100644 --- a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb +++ b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb @@ -21,7 +21,7 @@ class MultiParameterAttributesTest < ActiveSupport::TestCase permitted = params.permit book: [ :shipped_at, :price ] - assert permitted.permitted? + assert_predicate permitted, :permitted? assert_equal "2012", permitted[:book]["shipped_at(1i)"] assert_equal "3", permitted[:book]["shipped_at(2i)"] diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb index 49dede03c2..4d9fea7188 100644 --- a/actionpack/test/controller/parameters/mutators_test.rb +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -20,11 +20,11 @@ class ParametersMutatorsTest < ActiveSupport::TestCase test "delete retains permitted status" do @params.permit! - assert @params.delete(:person).permitted? + assert_predicate @params.delete(:person), :permitted? end test "delete retains unpermitted status" do - assert_not @params.delete(:person).permitted? + assert_not_predicate @params.delete(:person), :permitted? end test "delete returns the value when the key is present" do @@ -50,73 +50,73 @@ class ParametersMutatorsTest < ActiveSupport::TestCase test "delete_if retains permitted status" do @params.permit! - assert @params.delete_if { |k| k == "person" }.permitted? + assert_predicate @params.delete_if { |k| k == "person" }, :permitted? end test "delete_if retains unpermitted status" do - assert_not @params.delete_if { |k| k == "person" }.permitted? + assert_not_predicate @params.delete_if { |k| k == "person" }, :permitted? end test "extract! retains permitted status" do @params.permit! - assert @params.extract!(:person).permitted? + assert_predicate @params.extract!(:person), :permitted? end test "extract! retains unpermitted status" do - assert_not @params.extract!(:person).permitted? + assert_not_predicate @params.extract!(:person), :permitted? end test "keep_if retains permitted status" do @params.permit! - assert @params.keep_if { |k, v| k == "person" }.permitted? + assert_predicate @params.keep_if { |k, v| k == "person" }, :permitted? end test "keep_if retains unpermitted status" do - assert_not @params.keep_if { |k, v| k == "person" }.permitted? + assert_not_predicate @params.keep_if { |k, v| k == "person" }, :permitted? end test "reject! retains permitted status" do @params.permit! - assert @params.reject! { |k| k == "person" }.permitted? + assert_predicate @params.reject! { |k| k == "person" }, :permitted? end test "reject! retains unpermitted status" do - assert_not @params.reject! { |k| k == "person" }.permitted? + assert_not_predicate @params.reject! { |k| k == "person" }, :permitted? end test "select! retains permitted status" do @params.permit! - assert @params.select! { |k| k != "person" }.permitted? + assert_predicate @params.select! { |k| k != "person" }, :permitted? end test "select! retains unpermitted status" do - assert_not @params.select! { |k| k != "person" }.permitted? + assert_not_predicate @params.select! { |k| k != "person" }, :permitted? end test "slice! retains permitted status" do @params.permit! - assert @params.slice!(:person).permitted? + assert_predicate @params.slice!(:person), :permitted? end test "slice! retains unpermitted status" do - assert_not @params.slice!(:person).permitted? + assert_not_predicate @params.slice!(:person), :permitted? end test "transform_keys! retains permitted status" do @params.permit! - assert @params.transform_keys! { |k| k }.permitted? + assert_predicate @params.transform_keys! { |k| k }, :permitted? end test "transform_keys! retains unpermitted status" do - assert_not @params.transform_keys! { |k| k }.permitted? + assert_not_predicate @params.transform_keys! { |k| k }, :permitted? end test "transform_values! retains permitted status" do @params.permit! - assert @params.transform_values! { |v| v }.permitted? + assert_predicate @params.transform_values! { |v| v }, :permitted? end test "transform_values! retains unpermitted status" do - assert_not @params.transform_values! { |v| v }.permitted? + assert_not_predicate @params.transform_values! { |v| v }, :permitted? end end diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb index c9fcc483ee..ccc6bf9807 100644 --- a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb @@ -32,7 +32,7 @@ class NestedParametersPermitTest < ActiveSupport::TestCase permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages }, :id ] - assert permitted.permitted? + assert_predicate permitted, :permitted? assert_equal "Romeo and Juliet", permitted[:book][:title] assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index ebdaca0162..295f3a03ef 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -53,13 +53,13 @@ class ParametersPermitTest < ActiveSupport::TestCase test "if nothing is permitted, the hash becomes empty" do params = ActionController::Parameters.new(id: "1234") permitted = params.permit - assert permitted.permitted? - assert permitted.empty? + assert_predicate permitted, :permitted? + assert_empty permitted end test "key: permitted scalar values" do values = ["a", :a, nil] - values += [0, 1.0, 2**128, BigDecimal.new(1)] + values += [0, 1.0, 2**128, BigDecimal(1)] values += [true, false] values += [Date.today, Time.now, DateTime.now] values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__), @@ -227,7 +227,7 @@ class ParametersPermitTest < ActiveSupport::TestCase test "hashes in array values get wrapped" do params = ActionController::Parameters.new(foo: [{}, {}]) params[:foo].each do |hash| - assert !hash.permitted? + assert_not_predicate hash, :permitted? end end @@ -250,7 +250,7 @@ class ParametersPermitTest < ActiveSupport::TestCase permitted = params.permit(users: [:id]) permitted[:users] << { injected: 1 } - assert_not permitted[:users].last.permitted? + assert_not_predicate permitted[:users].last, :permitted? end test "fetch doesnt raise ParameterMissing exception if there is a default" do @@ -272,12 +272,12 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "not permitted is sticky beyond merges" do - assert !@params.merge(a: "b").permitted? + assert_not_predicate @params.merge(a: "b"), :permitted? end test "permitted is sticky beyond merges" do @params.permit! - assert @params.merge(a: "b").permitted? + assert_predicate @params.merge(a: "b"), :permitted? end test "merge with parameters" do @@ -288,12 +288,12 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "not permitted is sticky beyond merge!" do - assert_not @params.merge!(a: "b").permitted? + assert_not_predicate @params.merge!(a: "b"), :permitted? end test "permitted is sticky beyond merge!" do @params.permit! - assert @params.merge!(a: "b").permitted? + assert_predicate @params.merge!(a: "b"), :permitted? end test "merge! with parameters" do @@ -355,10 +355,10 @@ class ParametersPermitTest < ActiveSupport::TestCase test "permit is recursive" do @params.permit! - assert @params.permitted? - assert @params[:person].permitted? - assert @params[:person][:name].permitted? - assert @params[:person][:addresses][0].permitted? + assert_predicate @params, :permitted? + assert_predicate @params[:person], :permitted? + assert_predicate @params[:person][:name], :permitted? + assert_predicate @params[:person][:addresses][0], :permitted? end test "permitted takes a default value when Parameters.permit_all_parameters is set" do @@ -368,8 +368,8 @@ class ParametersPermitTest < ActiveSupport::TestCase age: "32", name: { first: "David", last: "Heinemeier Hansson" } }) - assert params.slice(:person).permitted? - assert params[:person][:name].permitted? + assert_predicate params.slice(:person), :permitted? + assert_predicate params[:person][:name], :permitted? ensure ActionController::Parameters.permit_all_parameters = false end @@ -500,9 +500,9 @@ class ParametersPermitTest < ActiveSupport::TestCase params = ActionController::Parameters.new(foo: "bar") assert params.permit(:foo).has_key?(:foo) - refute params.permit(foo: []).has_key?(:foo) - refute params.permit(foo: [:bar]).has_key?(:foo) - refute params.permit(foo: :bar).has_key?(:foo) + assert_not params.permit(foo: []).has_key?(:foo) + assert_not params.permit(foo: [:bar]).has_key?(:foo) + assert_not params.permit(foo: :bar).has_key?(:foo) end test "#permitted? is false by default" do diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb index 823f01d82a..7809d0f357 100644 --- a/actionpack/test/controller/parameters/serialization_test.rb +++ b/actionpack/test/controller/parameters/serialization_test.rb @@ -27,7 +27,7 @@ class ParametersSerializationTest < ActiveSupport::TestCase roundtripped = YAML.load(YAML.dump(params)) assert_equal params, roundtripped - assert_not roundtripped.permitted? + assert_not_predicate roundtripped, :permitted? end test "yaml backwardscompatible with psych 2.0.8 format" do @@ -37,7 +37,7 @@ class ParametersSerializationTest < ActiveSupport::TestCase end_of_yaml assert_equal :value, params[:key] - assert_not params.permitted? + assert_not_predicate params, :permitted? end test "yaml backwardscompatible with psych 2.0.9+ format" do @@ -50,6 +50,6 @@ class ParametersSerializationTest < ActiveSupport::TestCase end_of_yaml assert_equal :value, params[:key] - assert_not params.permitted? + assert_not_predicate params, :permitted? end end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 37a62edc15..fc21543049 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -415,7 +415,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = @last_modified get :conditional_hello assert_equal 304, @response.status.to_i - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal @last_modified, @response.headers["Last-Modified"] end @@ -430,7 +430,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT" get :conditional_hello assert_equal 200, @response.status.to_i - assert @response.body.present? + assert_predicate @response.body, :present? assert_equal @last_modified, @response.headers["Last-Modified"] end @@ -443,7 +443,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = @last_modified get :conditional_hello_with_record assert_equal 304, @response.status.to_i - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_not_nil @response.etag assert_equal @last_modified, @response.headers["Last-Modified"] end @@ -459,7 +459,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT" get :conditional_hello_with_record assert_equal 200, @response.status.to_i - assert @response.body.present? + assert_predicate @response.body, :present? assert_equal @last_modified, @response.headers["Last-Modified"] end @@ -472,7 +472,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = @last_modified get :conditional_hello_with_collection_of_records assert_equal 304, @response.status.to_i - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal @last_modified, @response.headers["Last-Modified"] end @@ -487,7 +487,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT" get :conditional_hello_with_collection_of_records assert_equal 200, @response.status.to_i - assert @response.body.present? + assert_predicate @response.body, :present? assert_equal @last_modified, @response.headers["Last-Modified"] end @@ -592,7 +592,7 @@ class EtagRenderTest < ActionController::TestCase end def strong_etag(record) - %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") + %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") end end @@ -682,27 +682,27 @@ class HeadRenderTest < ActionController::TestCase def test_head_created post :head_created - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_response :created end def test_head_created_with_application_json_content_type post :head_created_with_application_json_content_type - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal "application/json", @response.header["Content-Type"] assert_response :created end def test_head_ok_with_image_png_content_type post :head_ok_with_image_png_content_type - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal "image/png", @response.header["Content-Type"] assert_response :ok end def test_head_with_location_header get :head_with_location_header - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal "/foo", @response.headers["Location"] assert_response :ok end @@ -718,7 +718,7 @@ class HeadRenderTest < ActionController::TestCase end get :head_with_location_object - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"] assert_response :ok end @@ -726,14 +726,14 @@ class HeadRenderTest < ActionController::TestCase def test_head_with_custom_header get :head_with_custom_header - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal "something", @response.headers["X-Custom-Header"] assert_response :ok end def test_head_with_www_authenticate_header get :head_with_www_authenticate_header - assert @response.body.blank? + assert_predicate @response.body, :blank? assert_equal "something", @response.headers["WWW-Authenticate"] assert_response :ok end @@ -812,7 +812,7 @@ class HttpCacheForeverTest < ActionController::TestCase assert_response :ok assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"] assert_not_nil @response.etag - assert @response.weak_etag? + assert_predicate @response, :weak_etag? end def test_cache_with_private @@ -820,7 +820,7 @@ class HttpCacheForeverTest < ActionController::TestCase assert_response :ok assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"] assert_not_nil @response.etag - assert @response.weak_etag? + assert_predicate @response, :weak_etag? end def test_cache_response_code_with_if_modified_since diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index eb3d2f34a8..7a02c27c99 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -446,6 +446,19 @@ module RequestForgeryProtectionTests end end + def test_should_raise_for_post_with_null_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + exception = assert_raises(ActionController::InvalidAuthenticityToken) do + @request.set_header "HTTP_ORIGIN", "null" + post :index, params: { custom_authenticity_token: @token } + end + assert_match "The browser returned a 'null' origin for a request", exception.message + end + end + end + def test_should_block_post_with_origin_checking_and_wrong_origin old_logger = ActionController::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new @@ -733,7 +746,7 @@ class FreeCookieControllerTest < ActionController::TestCase test "should not emit a csrf-token meta tag" do SecureRandom.stub :base64, @token do get :meta - assert @response.body.blank? + assert_predicate @response.body, :blank? end end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 3d98237003..30bea64c55 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -307,7 +307,7 @@ class ResourcesTest < ActionController::TestCase set.draw do resources :messages do member do - match :mark , via: method + match :mark, via: method match :unmark, via: method end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index f09051b306..ec939e946a 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -213,7 +213,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal expected, ActiveSupport::JSON.decode(get(u)) end - def test_regexp_precidence + def test_regexp_precedence rs.draw do get "/whois/:domain", constraints: { domain: /\w+\.[\w\.]+/ }, @@ -1687,7 +1687,7 @@ class RouteSetTest < ActiveSupport::TestCase def test_routes_with_symbols set.draw do get "unnamed", controller: :pages, action: :show, name: :as_symbol - get "named" , controller: :pages, action: :show, name: :as_symbol, as: :named + get "named", controller: :pages, action: :show, name: :as_symbol, as: :named end assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/unnamed")) assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/named")) @@ -1893,7 +1893,7 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal({ controller: "blog", action: "show_date", year: "2006", month: "07", day: "28" }, controller.request.path_parameters) assert_equal("/blog/2006/07/25", controller.url_for(day: 25, only_path: true)) assert_equal("/blog/2005", controller.url_for(year: 2005, only_path: true)) - assert_equal("/blog/show/123", controller.url_for(action: "show" , id: 123, only_path: true)) + assert_equal("/blog/show/123", controller.url_for(action: "show", id: 123, only_path: true)) assert_equal("/blog/2006", controller.url_for(year: 2006, only_path: true)) assert_equal("/blog/2006", controller.url_for(year: 2006, month: nil, only_path: true)) end diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb index a96c9c519b..1709ab5f6d 100644 --- a/actionpack/test/controller/runner_test.rb +++ b/actionpack/test/controller/runner_test.rb @@ -17,8 +17,8 @@ module ActionDispatch def test_respond_to? runner = MyRunner.new(Class.new { def x; end }.new) - assert runner.respond_to?(:hi) - assert runner.respond_to?(:x) + assert_respond_to runner, :hi + assert_respond_to runner, :x end end end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index fd2399e433..7b1a52b277 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -178,7 +178,7 @@ class SendFileTest < ActionController::TestCase "image.jpg" => "image/jpeg", "image.tif" => "image/tiff", "image.gif" => "image/gif", - "movie.mpg" => "video/mpeg", + "movie.mp4" => "video/mp4", "file.zip" => "application/zip", "file.unk" => "application/octet-stream", "zip" => "application/octet-stream" diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 536c5ed97a..7d4850294d 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -670,7 +670,7 @@ XML assert_equal "bar", @request.params[:foo] post :no_op - assert @request.params[:foo].blank? + assert_predicate @request.params[:foo], :blank? end def test_filtered_parameters_reset_between_requests @@ -838,7 +838,7 @@ XML def test_fixture_file_upload_should_be_able_access_to_tempfile file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg") - assert file.respond_to?(:tempfile), "expected tempfile should respond on fixture file object, got nothing" + assert_respond_to file, :tempfile end def test_fixture_file_upload diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index a7c7356921..a1521da702 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -35,7 +35,6 @@ module ActionPack as: "blog" resources :people - #match 'legacy/people' => "people#index", :legacy => "true" get "symbols", controller: :symbols, action: :show, name: :as_symbol get "id_default(/:id)" => "foo#id_default", :id => 1 diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index cf11227897..e381abee36 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -288,7 +288,7 @@ module AbstractController kls = Class.new { include set.url_helpers } controller = kls.new - assert controller.respond_to?(:home_url) + assert_respond_to controller, :home_url assert_equal "http://www.basecamphq.com/home/sweet/home/again", controller.send(:home_url, host: "www.basecamphq.com", user: "again") diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index 0f79c83b6d..ca83b850d5 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -19,7 +19,7 @@ class UrlRewriterTests < ActionController::TestCase def setup @params = {} - @rewriter = Rewriter.new(@request) #.new(@request, @params) + @rewriter = Rewriter.new(@request) @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do ActiveSupport::Deprecation.silence do diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb new file mode 100644 index 0000000000..7c4a65a633 --- /dev/null +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -0,0 +1,368 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ContentSecurityPolicyTest < ActiveSupport::TestCase + def setup + @policy = ActionDispatch::ContentSecurityPolicy.new + end + + def test_build + assert_equal ";", @policy.build + + @policy.script_src :self + assert_equal "script-src 'self';", @policy.build + end + + def test_dup + @policy.img_src :self + @policy.block_all_mixed_content + @policy.upgrade_insecure_requests + @policy.sandbox + copied = @policy.dup + assert_equal copied.build, @policy.build + end + + def test_mappings + @policy.script_src :data + assert_equal "script-src data:;", @policy.build + + @policy.script_src :mediastream + assert_equal "script-src mediastream:;", @policy.build + + @policy.script_src :blob + assert_equal "script-src blob:;", @policy.build + + @policy.script_src :filesystem + assert_equal "script-src filesystem:;", @policy.build + + @policy.script_src :self + assert_equal "script-src 'self';", @policy.build + + @policy.script_src :unsafe_inline + assert_equal "script-src 'unsafe-inline';", @policy.build + + @policy.script_src :unsafe_eval + assert_equal "script-src 'unsafe-eval';", @policy.build + + @policy.script_src :none + assert_equal "script-src 'none';", @policy.build + + @policy.script_src :strict_dynamic + assert_equal "script-src 'strict-dynamic';", @policy.build + + @policy.script_src :none, :report_sample + assert_equal "script-src 'none' 'report-sample';", @policy.build + end + + def test_fetch_directives + @policy.child_src :self + assert_match %r{child-src 'self'}, @policy.build + + @policy.child_src false + assert_no_match %r{child-src}, @policy.build + + @policy.connect_src :self + assert_match %r{connect-src 'self'}, @policy.build + + @policy.connect_src false + assert_no_match %r{connect-src}, @policy.build + + @policy.default_src :self + assert_match %r{default-src 'self'}, @policy.build + + @policy.default_src false + assert_no_match %r{default-src}, @policy.build + + @policy.font_src :self + assert_match %r{font-src 'self'}, @policy.build + + @policy.font_src false + assert_no_match %r{font-src}, @policy.build + + @policy.frame_src :self + assert_match %r{frame-src 'self'}, @policy.build + + @policy.frame_src false + assert_no_match %r{frame-src}, @policy.build + + @policy.img_src :self + assert_match %r{img-src 'self'}, @policy.build + + @policy.img_src false + assert_no_match %r{img-src}, @policy.build + + @policy.manifest_src :self + assert_match %r{manifest-src 'self'}, @policy.build + + @policy.manifest_src false + assert_no_match %r{manifest-src}, @policy.build + + @policy.media_src :self + assert_match %r{media-src 'self'}, @policy.build + + @policy.media_src false + assert_no_match %r{media-src}, @policy.build + + @policy.object_src :self + assert_match %r{object-src 'self'}, @policy.build + + @policy.object_src false + assert_no_match %r{object-src}, @policy.build + + @policy.script_src :self + assert_match %r{script-src 'self'}, @policy.build + + @policy.script_src false + assert_no_match %r{script-src}, @policy.build + + @policy.style_src :self + assert_match %r{style-src 'self'}, @policy.build + + @policy.style_src false + assert_no_match %r{style-src}, @policy.build + + @policy.worker_src :self + assert_match %r{worker-src 'self'}, @policy.build + + @policy.worker_src false + assert_no_match %r{worker-src}, @policy.build + end + + def test_document_directives + @policy.base_uri "https://example.com" + assert_match %r{base-uri https://example\.com;}, @policy.build + + @policy.plugin_types "application/x-shockwave-flash" + assert_match %r{plugin-types application/x-shockwave-flash;}, @policy.build + + @policy.sandbox + assert_match %r{sandbox;}, @policy.build + + @policy.sandbox "allow-scripts", "allow-modals" + assert_match %r{sandbox allow-scripts allow-modals;}, @policy.build + + @policy.sandbox false + assert_no_match %r{sandbox}, @policy.build + end + + def test_navigation_directives + @policy.form_action :self + assert_match %r{form-action 'self';}, @policy.build + + @policy.frame_ancestors :self + assert_match %r{frame-ancestors 'self';}, @policy.build + end + + def test_reporting_directives + @policy.report_uri "/violations" + assert_match %r{report-uri /violations;}, @policy.build + end + + def test_other_directives + @policy.block_all_mixed_content + assert_match %r{block-all-mixed-content;}, @policy.build + + @policy.block_all_mixed_content false + assert_no_match %r{block-all-mixed-content}, @policy.build + + @policy.require_sri_for :script, :style + assert_match %r{require-sri-for script style;}, @policy.build + + @policy.require_sri_for "script", "style" + assert_match %r{require-sri-for script style;}, @policy.build + + @policy.require_sri_for + assert_no_match %r{require-sri-for}, @policy.build + + @policy.upgrade_insecure_requests + assert_match %r{upgrade-insecure-requests;}, @policy.build + + @policy.upgrade_insecure_requests false + assert_no_match %r{upgrade-insecure-requests}, @policy.build + end + + def test_multiple_sources + @policy.script_src :self, :https + assert_equal "script-src 'self' https:;", @policy.build + end + + def test_multiple_directives + @policy.script_src :self, :https + @policy.style_src :self, :https + assert_equal "script-src 'self' https:; style-src 'self' https:;", @policy.build + end + + def test_dynamic_directives + request = Struct.new(:host).new("www.example.com") + controller = Struct.new(:request).new(request) + + @policy.script_src -> { request.host } + assert_equal "script-src www.example.com;", @policy.build(controller) + end + + def test_mixed_static_and_dynamic_directives + @policy.script_src :self, -> { "foo.com" }, "bar.com" + assert_equal "script-src 'self' foo.com bar.com;", @policy.build(Object.new) + end + + def test_invalid_directive_source + exception = assert_raises(ArgumentError) do + @policy.script_src [:self] + end + + assert_equal "Invalid content security policy source: [:self]", exception.message + end + + def test_missing_context_for_dynamic_source + @policy.script_src -> { request.host } + + exception = assert_raises(RuntimeError) do + @policy.build + end + + assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message + end + + def test_raises_runtime_error_when_unexpected_source + @policy.plugin_types [:flash] + + exception = assert_raises(RuntimeError) do + @policy.build + end + + assert_match %r{\AUnexpected content security policy source:}, exception.message + end +end + +class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + content_security_policy only: :inline do |p| + p.default_src "https://example.com" + end + + content_security_policy only: :conditional, if: :condition? do |p| + p.default_src "https://true.example.com" + end + + content_security_policy only: :conditional, unless: :condition? do |p| + p.default_src "https://false.example.com" + end + + content_security_policy only: :report_only do |p| + p.report_uri "/violations" + end + + content_security_policy_report_only only: :report_only + + def index + head :ok + end + + def inline + head :ok + end + + def conditional + head :ok + end + + def report_only + head :ok + end + + private + def condition? + params[:condition] == "true" + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "content_security_policy_integration_test" do + get "/", to: "policy#index" + get "/inline", to: "policy#inline" + get "/conditional", to: "policy#conditional" + get "/report-only", to: "policy#report_only" + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src :self + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_generates_content_security_policy_header + get "/" + assert_policy "default-src 'self';" + end + + def test_generates_inline_content_security_policy + get "/inline" + assert_policy "default-src https://example.com;" + end + + def test_generates_conditional_content_security_policy + get "/conditional", params: { condition: "true" } + assert_policy "default-src https://true.example.com;" + + get "/conditional", params: { condition: "false" } + assert_policy "default-src https://false.example.com;" + end + + def test_generates_report_only_content_security_policy + get "/report-only" + assert_policy "default-src 'self'; report-uri /violations;", report_only: true + end + + private + + def env_config + Rails.application.env_config + end + + def content_security_policy + env_config["action_dispatch.content_security_policy"] + end + + def content_security_policy=(policy) + env_config["action_dispatch.content_security_policy"] = policy + end + + 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" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil response.headers[unexpected_header] + assert_equal expected, response.headers[expected_header] + end +end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 40cbad3b0d..94cff10fe4 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -36,6 +36,12 @@ class CookieJarTest < ActiveSupport::TestCase assert_equal "bar", request.cookie_jar.fetch(:foo) end + def test_to_hash + request.cookie_jar["foo"] = "bar" + assert_equal({ "foo" => "bar" }, request.cookie_jar.to_hash) + assert_equal({ "foo" => "bar" }, request.cookie_jar.to_h) + end + def test_fetch_type_error assert_raises(KeyError) do request.cookie_jar.fetch(:omglolwut) @@ -319,7 +325,7 @@ class CookiesTest < ActionController::TestCase def test_setting_the_same_value_to_cookie request.cookies[:user_name] = "david" get :authenticate - assert_predicate response.cookies, :empty? + assert_empty response.cookies end def test_setting_the_same_value_to_permanent_cookie @@ -401,7 +407,7 @@ class CookiesTest < ActionController::TestCase def test_delete_unexisting_cookie request.cookies.clear get :delete_cookie - assert_predicate @response.cookies, :empty? + assert_empty @response.cookies end def test_deleted_cookie_predicate diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index 2901148a9e..a9a56f205f 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -73,7 +73,7 @@ module ActionController } latch.wait - assert @response.headers.frozen? + assert_predicate @response.headers, :frozen? e = assert_raises(ActionDispatch::IllegalStateError) do @response.headers["Content-Length"] = "zomg" end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 90e95e972d..6167ea46df 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -30,21 +30,21 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse text with trailing star at the beginning" do accept = "text/*, text/html, application/json, multipart/form-data" - expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]] + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]] parsed = Mime::Type.parse(accept) - assert_equal expect, parsed + assert_equal expect.map(&:to_s), parsed.map(&:to_s) end test "parse text with trailing star in the end" do accept = "text/html, application/json, multipart/form-data, text/*" - expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]] + expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]] parsed = Mime::Type.parse(accept) - assert_equal expect, parsed + assert_equal expect.map(&:to_s), parsed.map(&:to_s) end test "parse text with trailing star" do accept = "text/*" - expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]] + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]] parsed = Mime::Type.parse(accept) assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort! end @@ -159,7 +159,7 @@ class MimeTypeTest < ActiveSupport::TestCase types.each do |type| mime = Mime[type] - assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?" + assert_respond_to mime, "#{type}?" assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?" invalid_types = types - [type] invalid_types.delete(:html) diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 7b6ce31f29..bf5a74e694 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -22,6 +22,7 @@ module ActionDispatch s["foo"] = "bar" assert_equal "bar", s["foo"] assert_equal({ "foo" => "bar" }, s.to_hash) + assert_equal({ "foo" => "bar" }, s.to_h) end def test_create_merges_old diff --git a/actionpack/test/dispatch/request_id_test.rb b/actionpack/test/dispatch/request_id_test.rb index aa3175c986..9df4712dab 100644 --- a/actionpack/test/dispatch/request_id_test.rb +++ b/actionpack/test/dispatch/request_id_test.rb @@ -11,6 +11,11 @@ class RequestIdTest < ActiveSupport::TestCase assert_equal "X-Hacked-HeaderStuff", stub_request("HTTP_X_REQUEST_ID" => "; X-Hacked-Header: Stuff").request_id end + test "accept Apache mod_unique_id format" do + mod_unique_id = "abcxyz@ABCXYZ-0123456789" + assert_equal mod_unique_id, stub_request("HTTP_X_REQUEST_ID" => mod_unique_id).request_id + end + test "ensure that 255 char limit on the request id is being enforced" do assert_equal "X" * 255, stub_request("HTTP_X_REQUEST_ID" => "X" * 500).request_id end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 2a18395aac..84a2d1f69e 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -329,20 +329,20 @@ class RequestPort < BaseRequestTest test "standard_port?" do request = stub_request - assert !request.ssl? - assert request.standard_port? + assert_not_predicate request, :ssl? + assert_predicate request, :standard_port? request = stub_request "HTTPS" => "on" - assert request.ssl? - assert request.standard_port? + assert_predicate request, :ssl? + assert_predicate request, :standard_port? request = stub_request "HTTP_HOST" => "www.example.org:8080" - assert !request.ssl? - assert !request.standard_port? + assert_not_predicate request, :ssl? + assert_not_predicate request, :standard_port? request = stub_request "HTTP_HOST" => "www.example.org:8443", "HTTPS" => "on" - assert request.ssl? - assert !request.standard_port? + assert_predicate request, :ssl? + assert_not_predicate request, :standard_port? end test "optional port" do @@ -571,7 +571,7 @@ end class LocalhostTest < BaseRequestTest test "IPs that match localhost" do request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1") - assert request.local? + assert_predicate request, :local? end end @@ -643,37 +643,37 @@ class RequestProtocol < BaseRequestTest test "xml http request" do request = stub_request - assert !request.xml_http_request? - assert !request.xhr? + assert_not_predicate request, :xml_http_request? + assert_not_predicate request, :xhr? request = stub_request "HTTP_X_REQUESTED_WITH" => "DefinitelyNotAjax1.0" - assert !request.xml_http_request? - assert !request.xhr? + assert_not_predicate request, :xml_http_request? + assert_not_predicate request, :xhr? request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - assert request.xml_http_request? - assert request.xhr? + assert_predicate request, :xml_http_request? + assert_predicate request, :xhr? end test "reports ssl" do - assert !stub_request.ssl? - assert stub_request("HTTPS" => "on").ssl? + assert_not_predicate stub_request, :ssl? + assert_predicate stub_request("HTTPS" => "on"), :ssl? end test "reports ssl when proxied via lighttpd" do - assert stub_request("HTTP_X_FORWARDED_PROTO" => "https").ssl? + assert_predicate stub_request("HTTP_X_FORWARDED_PROTO" => "https"), :ssl? end test "scheme returns https when proxied" do request = stub_request "rack.url_scheme" => "http" - assert !request.ssl? + assert_not_predicate request, :ssl? assert_equal "http", request.scheme request = stub_request( "rack.url_scheme" => "http", "HTTP_X_FORWARDED_PROTO" => "https" ) - assert request.ssl? + assert_predicate request, :ssl? assert_equal "https", request.scheme end end @@ -700,7 +700,7 @@ class RequestMethod < BaseRequestTest assert_equal "GET", request.request_method assert_equal "GET", request.env["REQUEST_METHOD"] - assert request.get? + assert_predicate request, :get? end test "invalid http method raises exception" do @@ -748,7 +748,7 @@ class RequestMethod < BaseRequestTest assert_equal "POST", request.method assert_equal "PATCH", request.request_method - assert request.patch? + assert_predicate request, :patch? end test "post masquerading as put" do @@ -758,12 +758,12 @@ class RequestMethod < BaseRequestTest ) assert_equal "POST", request.method assert_equal "PUT", request.request_method - assert request.put? + assert_predicate request, :put? end test "post uneffected by local inflections" do existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup - existing_acronym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup + assert_deprecated { ActiveSupport::Inflector.inflections.acronym_regex.dup } begin ActiveSupport::Inflector.inflections do |inflect| inflect.acronym "POS" @@ -772,12 +772,12 @@ class RequestMethod < BaseRequestTest request = stub_request "REQUEST_METHOD" => "POST" assert_equal :post, ActionDispatch::Request::HTTP_METHOD_LOOKUP["POST"] assert_equal :post, request.method_symbol - assert request.post? + assert_predicate request, :post? ensure # Reset original acronym set ActiveSupport::Inflector.inflections do |inflect| inflect.send(:instance_variable_set, "@acronyms", existing_acronyms) - inflect.send(:instance_variable_set, "@acronym_regex", existing_acronym_regex) + inflect.send(:define_acronym_regex_patterns) end end end @@ -785,50 +785,44 @@ end class RequestFormat < BaseRequestTest test "xml format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :xml }) do - assert_equal Mime[:xml], request.format - end + request = stub_request "QUERY_STRING" => "format=xml" + + assert_equal Mime[:xml], request.format end test "xhtml format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :xhtml }) do - assert_equal Mime[:html], request.format - end + request = stub_request "QUERY_STRING" => "format=xhtml" + + assert_equal Mime[:html], request.format end test "txt format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :txt }) do - assert_equal Mime[:text], request.format - end + request = stub_request "QUERY_STRING" => "format=txt" + + assert_equal Mime[:text], request.format end test "XMLHttpRequest" do request = stub_request( "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(",") + "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(","), + "QUERY_STRING" => "" ) - assert_called(request, :parameters, times: 1, returns: {}) do - assert request.xhr? - assert_equal Mime[:js], request.format - end + assert_predicate request, :xhr? + assert_equal Mime[:js], request.format end test "can override format with parameter negative" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :txt }) do - assert !request.format.xml? - end + request = stub_request("QUERY_STRING" => "format=txt") + + assert_not_predicate request.format, :xml? end test "can override format with parameter positive" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :xml }) do - assert request.format.xml? - end + request = stub_request("QUERY_STRING" => "format=xml") + + assert_predicate request.format, :xml? end test "formats text/html with accept header" do @@ -853,40 +847,37 @@ class RequestFormat < BaseRequestTest end test "formats format:text with accept header" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :txt }) do - assert_equal [Mime[:text]], request.formats - end + request = stub_request("QUERY_STRING" => "format=txt") + + assert_equal [Mime[:text]], request.formats end test "formats format:unknown with accept header" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :unknown }) do - assert_instance_of Mime::NullType, request.format - end + request = stub_request("QUERY_STRING" => "format=unknown") + + assert_instance_of Mime::NullType, request.format end test "format is not nil with unknown format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :hello }) do - assert request.format.nil? - assert_not request.format.html? - assert_not request.format.xml? - assert_not request.format.json? - end + request = stub_request("QUERY_STRING" => "format=hello") + + assert_nil request.format + assert_not_predicate request.format, :html? + assert_not_predicate request.format, :xml? + assert_not_predicate request.format, :json? end test "format does not throw exceptions when malformed parameters" do request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") assert request.formats - assert request.format.html? + assert_predicate request.format, :html? end test "formats with xhr request" do - request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:js]], request.formats - end + request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "" + + assert_equal [Mime[:js]], request.formats end test "ignore_accept_header" do @@ -894,62 +885,58 @@ class RequestFormat < BaseRequestTest ActionDispatch::Request.ignore_accept_header = true begin - request = stub_request "HTTP_ACCEPT" => "application/xml" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + request = stub_request "HTTP_ACCEPT" => "application/xml", + "QUERY_STRING" => "" - request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + assert_equal [ Mime[:html] ], request.formats - request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy", + "QUERY_STRING" => "" - request = stub_request "HTTP_ACCEPT" => "application/jxw" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "application/jxw", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats request = stub_request "HTTP_ACCEPT" => "application/xml", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:js] ], request.formats - end + assert_equal [ Mime[:js] ], request.formats request = stub_request "HTTP_ACCEPT" => "application/xml", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - assert_called(request, :parameters, times: 2, returns: { format: :json }) do - assert_equal [ Mime[:json] ], request.formats - end + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "format=json" + + assert_equal [ Mime[:json] ], request.formats ensure ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end test "format taken from the path extension" do - request = stub_request "PATH_INFO" => "/foo.xml" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:xml]], request.formats - end + request = stub_request "PATH_INFO" => "/foo.xml", "QUERY_STRING" => "" - request = stub_request "PATH_INFO" => "/foo.123" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:html]], request.formats - end + assert_equal [Mime[:xml]], request.formats + + request = stub_request "PATH_INFO" => "/foo.123", "QUERY_STRING" => "" + + assert_equal [Mime[:html]], request.formats end test "formats from accept headers have higher precedence than path extension" do request = stub_request "HTTP_ACCEPT" => "application/json", - "PATH_INFO" => "/foo.xml" + "PATH_INFO" => "/foo.xml", + "QUERY_STRING" => "" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:json]], request.formats - end + assert_equal [Mime[:json]], request.formats end end @@ -997,15 +984,14 @@ end class RequestParameters < BaseRequestTest test "parameters" do - request = stub_request + request = stub_request "CONTENT_TYPE" => "application/json", + "CONTENT_LENGTH" => 9, + "RAW_POST_DATA" => '{"foo":1}', + "QUERY_STRING" => "bar=2" - assert_called(request, :request_parameters, times: 2, returns: { "foo" => 1 }) do - assert_called(request, :query_parameters, times: 2, returns: { "bar" => 2 }) do - assert_equal({ "foo" => 1, "bar" => 2 }, request.parameters) - assert_equal({ "foo" => 1 }, request.request_parameters) - assert_equal({ "bar" => 2 }, request.query_parameters) - end - end + assert_equal({ "foo" => 1, "bar" => "2" }, request.parameters) + assert_equal({ "foo" => 1 }, request.request_parameters) + assert_equal({ "bar" => "2" }, request.query_parameters) end test "parameters not accessible after rack parse error" do @@ -1248,8 +1234,8 @@ class RequestVariant < BaseRequestTest test "setting variant to a symbol" do @request.variant = :phone - assert @request.variant.phone? - assert_not @request.variant.tablet? + assert_predicate @request.variant, :phone? + assert_not_predicate @request.variant, :tablet? assert @request.variant.any?(:phone, :tablet) assert_not @request.variant.any?(:tablet, :desktop) end @@ -1257,9 +1243,9 @@ class RequestVariant < BaseRequestTest test "setting variant to an array of symbols" do @request.variant = [:phone, :tablet] - assert @request.variant.phone? - assert @request.variant.tablet? - assert_not @request.variant.desktop? + assert_predicate @request.variant, :phone? + assert_predicate @request.variant, :tablet? + assert_not_predicate @request.variant, :desktop? assert @request.variant.any?(:tablet, :desktop) assert_not @request.variant.any?(:desktop, :watch) end @@ -1267,8 +1253,8 @@ class RequestVariant < BaseRequestTest test "clearing variant" do @request.variant = nil - assert @request.variant.empty? - assert_not @request.variant.phone? + assert_empty @request.variant + assert_not_predicate @request.variant, :phone? assert_not @request.variant.any?(:phone, :tablet) end @@ -1287,13 +1273,13 @@ end class RequestFormData < BaseRequestTest test "media_type is from the FORM_DATA_MEDIA_TYPES array" do - assert stub_request("CONTENT_TYPE" => "application/x-www-form-urlencoded").form_data? - assert stub_request("CONTENT_TYPE" => "multipart/form-data").form_data? + assert_predicate stub_request("CONTENT_TYPE" => "application/x-www-form-urlencoded"), :form_data? + assert_predicate stub_request("CONTENT_TYPE" => "multipart/form-data"), :form_data? end test "media_type is not from the FORM_DATA_MEDIA_TYPES array" do - assert !stub_request("CONTENT_TYPE" => "application/xml").form_data? - assert !stub_request("CONTENT_TYPE" => "multipart/related").form_data? + assert_not_predicate stub_request("CONTENT_TYPE" => "application/xml"), :form_data? + assert_not_predicate stub_request("CONTENT_TYPE" => "multipart/related"), :form_data? end test "no Content-Type header is provided and the request_method is POST" do @@ -1301,7 +1287,7 @@ class RequestFormData < BaseRequestTest assert_equal "", request.media_type assert_equal "POST", request.request_method - assert !request.form_data? + assert_not_predicate request, :form_data? end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index c4ee3add2a..4c8d528507 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -15,13 +15,13 @@ class ResponseTest < ActiveSupport::TestCase @response.await_commit } @response.commit! - assert @response.committed? + assert_predicate @response, :committed? assert t.join(0.5) end def test_stream_close @response.stream.close - assert @response.stream.closed? + assert_predicate @response.stream, :closed? end def test_stream_write @@ -257,9 +257,9 @@ class ResponseTest < ActiveSupport::TestCase } resp.to_a - assert resp.etag? - assert resp.weak_etag? - assert_not resp.strong_etag? + assert_predicate resp, :etag? + assert_predicate resp, :weak_etag? + assert_not_predicate resp, :strong_etag? assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag) assert_equal({ public: true }, resp.cache_control) @@ -275,9 +275,9 @@ class ResponseTest < ActiveSupport::TestCase } resp.to_a - assert resp.etag? - assert_not resp.weak_etag? - assert resp.strong_etag? + assert_predicate resp, :etag? + assert_not_predicate resp, :weak_etag? + assert_predicate resp, :strong_etag? assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag) end @@ -311,13 +311,16 @@ class ResponseTest < ActiveSupport::TestCase end end - test "read x_frame_options, x_content_type_options and x_xss_protection" do + test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies, referrer_policy" do original_default_headers = ActionDispatch::Response.default_headers begin ActionDispatch::Response.default_headers = { "X-Frame-Options" => "DENY", "X-Content-Type-Options" => "nosniff", - "X-XSS-Protection" => "1;" + "X-XSS-Protection" => "1;", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" } resp = ActionDispatch::Response.create.tap { |response| response.body = "Hello" @@ -327,6 +330,9 @@ class ResponseTest < ActiveSupport::TestCase assert_equal("DENY", resp.headers["X-Frame-Options"]) assert_equal("nosniff", resp.headers["X-Content-Type-Options"]) assert_equal("1;", resp.headers["X-XSS-Protection"]) + assert_equal("noopen", resp.headers["X-Download-Options"]) + assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"]) + assert_equal("strict-origin-when-cross-origin", resp.headers["Referrer-Policy"]) ensure ActionDispatch::Response.default_headers = original_default_headers end @@ -350,7 +356,7 @@ class ResponseTest < ActiveSupport::TestCase end test "respond_to? accepts include_private" do - assert_not @response.respond_to?(:method_missing) + assert_not_respond_to @response, :method_missing assert @response.respond_to?(:method_missing, true) end diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb index a5198f2f13..009b6d9bc3 100644 --- a/actionpack/test/dispatch/routing_assertions_test.rb +++ b/actionpack/test/dispatch/routing_assertions_test.rb @@ -52,6 +52,8 @@ class RoutingAssertionsTest < ActionController::TestCase end mount engine => "/shelf" + + get "/shelf/foo", controller: "query_articles", action: "index" end end @@ -154,6 +156,10 @@ class RoutingAssertionsTest < ActionController::TestCase assert_match err.message, "This is a really bad msg" end + def test_assert_recognizes_continue_to_recoginize_after_it_tried_engines + assert_recognizes({ controller: "query_articles", action: "index" }, "/shelf/foo") + end + def test_assert_routing assert_routing("/articles", controller: "articles", action: "index") end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 44f902c163..4222eb4eb7 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3313,7 +3313,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end get "/search" - assert !@request.params[:action].frozen? + assert_not_predicate @request.params[:action], :frozen? end def test_multiple_positional_args_with_the_same_name @@ -4225,7 +4225,7 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest end end - #include Routes.url_helpers + # include Routes.url_helpers APP = build_app Routes def app; APP end @@ -4267,7 +4267,7 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest def app; APP end test "enabled when not mounted and default_url_options is empty" do - assert Routes.url_helpers.optimize_routes_generation? + assert_predicate Routes.url_helpers, :optimize_routes_generation? end test "named route called as singleton method" do @@ -5057,3 +5057,40 @@ class TestRecognizePath < ActionDispatch::IntegrationTest Routes.recognize_path(*args) end end + +class TestRelativeUrlRootGeneration < ActionDispatch::IntegrationTest + config = ActionDispatch::Routing::RouteSet::Config.new("/blog", false) + + stub_controllers(config) do |routes| + Routes = routes + + routes.draw do + get "/", to: "posts#index", as: :posts + get "/:id", to: "posts#show", as: :post + end + end + + include Routes.url_helpers + + APP = build_app Routes + + def app + APP + end + + def test_url_helpers + assert_equal "/blog/", posts_path({}) + assert_equal "/blog/", Routes.url_helpers.posts_path({}) + + assert_equal "/blog/1", post_path(id: "1") + assert_equal "/blog/1", Routes.url_helpers.post_path(id: "1") + end + + def test_optimized_url_helpers + assert_equal "/blog/", posts_path + assert_equal "/blog/", Routes.url_helpers.posts_path + + assert_equal "/blog/1", post_path("1") + assert_equal "/blog/1", Routes.url_helpers.post_path("1") + end +end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index cf51c47068..e34426a471 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -8,11 +8,14 @@ require "active_support/messages/rotation_configuration" class CookieStoreTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" SessionSecret = "b3c631c314c0bbca50c1b2843150fe33" - Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret) + SessionSalt = "authenticated encrypted cookie" + + Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000) Rotations = ActiveSupport::Messages::RotationConfiguration.new - Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: "SHA1") - SignedBar = Verifier.generate(foo: "bar", session_id: SecureRandom.hex(16)) + Encryptor = ActiveSupport::MessageEncryptor.new( + Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal + ) class TestController < ActionController::Base def no_session_access @@ -25,12 +28,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def set_session_value session[:foo] = "bar" - render plain: Rack::Utils.escape(Verifier.generate(session.to_hash)) - end - - def set_session_value_expires_in_five_hours - session[:foo] = "bar" - render plain: Rack::Utils.escape(Verifier.generate(session.to_hash, expires_in: 5.hours)) + render body: nil end def get_session_value @@ -72,19 +70,35 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end end + def parse_cookie_from_header + cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/) + cookie_matches && cookie_matches[1] + end + + def assert_session_cookie(cookie_string, contents) + assert_includes headers["Set-Cookie"], cookie_string + + session_value = parse_cookie_from_header + session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil + + assert_not_nil session_data, "session failed to decrypt" + assert_equal session_data.slice(*contents.keys), contents + end + def test_setting_session_value with_test_route_set do get "/set_session_value" + assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" end end def test_getting_session_value with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/get_session_value" + assert_response :success assert_equal 'foo: "bar"', response.body end @@ -92,8 +106,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_getting_session_id with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/persistent_session_id" + assert_response :success assert_equal 32, response.body.size session_id = response.body @@ -106,8 +121,12 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_disregards_tampered_sessions with_test_route_set do - cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780" + encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal) + + cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc") + get "/get_session_value" + assert_response :success assert_equal "foo: nil", response.body end @@ -135,19 +154,19 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_does_set_secure_cookies_over_https with_test_route_set(secure: true) do get "/set_session_value", headers: { "HTTPS" => "on" } + assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar" end end # {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"} - SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c" + EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw==" def test_deserializes_unloaded_classes_on_get_id with_test_route_set do with_autoload_path "session_autoload_test" do - cookies[SessionKey] = SignedSerializedCookie + cookies[SessionKey] = EncryptedSerializedCookie get "/get_session_id" assert_response :success assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class" @@ -158,7 +177,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_deserializes_unloaded_classes_on_get_value with_test_route_set do with_autoload_path "session_autoload_test" do - cookies[SessionKey] = SignedSerializedCookie + cookies[SessionKey] = EncryptedSerializedCookie get "/get_session_value" assert_response :success assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" @@ -197,8 +216,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest get "/set_session_value" assert_response :success session_payload = response.body - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/call_reset_session" assert_response :success @@ -216,8 +234,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/set_session_value" assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/get_class_after_reset_session" assert_response :success @@ -239,8 +256,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/set_session_value" assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/call_session_clear" assert_response :success @@ -253,7 +269,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_persistent_session_id with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/persistent_session_id" assert_response :success assert_equal 32, response.body.size @@ -268,8 +284,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_setting_session_id_to_nil_is_respected with_test_route_set do - cookies[SessionKey] = SignedBar - + get "/set_session_value" get "/get_session_id" sid = response.body assert_equal 36, sid.size @@ -283,31 +298,53 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set(expire_after: 5.hours) do # First request accesses the session time = Time.local(2008, 4, 24) - cookie_body = nil Time.stub :now, time do expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - cookies[SessionKey] = SignedBar + get "/set_session_value" - get "/set_session_value_expires_in_five_hours" assert_response :success - - cookie_body = response.body - assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" end # Second request does not access the session - time = Time.local(2008, 4, 25) + time = time + 3.hours Time.stub :now, time do expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") get "/no_session_access" + + assert_response :success + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + end + end + + def test_session_store_with_expire_after_does_not_accept_expired_session + with_test_route_set(expire_after: 5.hours) do + # First request accesses the session + time = Time.local(2017, 11, 12) + + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + + get "/set_session_value" + get "/get_session_value" + assert_response :success + assert_equal 'foo: "bar"', response.body + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + + # Second request is beyond the expiry time and the session is invalidated + time += 5.hours + 1.minute - assert_equal "_myapp_session=#{cookies[SessionKey]}; path=/; expires=#{expected_expiry}; HttpOnly", - headers["Set-Cookie"] + Time.stub :now, time do + get "/get_session_value" + + assert_response :success + assert_equal "foo: nil", response.body end end end @@ -347,8 +384,14 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def get(path, *args) args[0] ||= {} args[0][:headers] ||= {} - args[0][:headers]["action_dispatch.key_generator"] ||= Generator - args[0][:headers]["action_dispatch.cookies_rotations"] ||= Rotations + args[0][:headers].tap do |config| + config["action_dispatch.secret_key_base"] = SessionSecret + config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt + config["action_dispatch.use_authenticated_cookie_encryption"] = true + + config["action_dispatch.key_generator"] ||= Generator + config["action_dispatch.cookies_rotations"] ||= Rotations + end super(path, *args) end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb index 75feae6fe0..a824ee0c84 100644 --- a/actionpack/test/dispatch/system_testing/driver_test.rb +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -12,7 +12,8 @@ class DriverTest < ActiveSupport::TestCase test "initializing the driver with a browser" do driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) assert_equal :selenium, driver.instance_variable_get(:@name) - assert_equal :chrome, driver.instance_variable_get(:@browser) + assert_equal :chrome, driver.instance_variable_get(:@browser).name + assert_nil driver.instance_variable_get(:@browser).options assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end @@ -20,7 +21,15 @@ class DriverTest < ActiveSupport::TestCase test "initializing the driver with a headless chrome" do driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) assert_equal :selenium, driver.instance_variable_get(:@name) - assert_equal :headless_chrome, driver.instance_variable_get(:@browser) + assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a headless firefox" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb index 2afda31cf5..264844fc7d 100644 --- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -35,6 +35,11 @@ class ScreenshotHelperTest < ActiveSupport::TestCase end end + test "defaults to simple output for the screenshot" do + new_test = DrivenBySeleniumWithChrome.new("x") + assert_equal "simple", new_test.send(:output_type) + end + test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do begin original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] @@ -42,6 +47,8 @@ class ScreenshotHelperTest < ActiveSupport::TestCase new_test = DrivenBySeleniumWithChrome.new("x") + assert_equal "artifact", new_test.send(:output_type) + Rails.stub :root, Pathname.getwd do new_test.stub :passed?, false do assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image) diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb index 1866225fc1..95e411faf4 100644 --- a/actionpack/test/dispatch/system_testing/server_test.rb +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -6,10 +6,27 @@ require "action_dispatch/system_testing/server" class ServerTest < ActiveSupport::TestCase setup do - ActionDispatch::SystemTesting::Server.new.run + @old_capybara_server = Capybara.server end test "port is always included" do + ActionDispatch::SystemTesting::Server.new.run assert Capybara.always_include_port, "expected Capybara.always_include_port to be true" end + + test "server is changed from `default` to `puma`" do + Capybara.server = :default + ActionDispatch::SystemTesting::Server.new.run + refute_equal Capybara.server, Capybara.servers[:default] + end + + test "server is not changed to `puma` when is different than default" do + Capybara.server = :webrick + ActionDispatch::SystemTesting::Server.new.run + assert_equal Capybara.server, Capybara.servers[:webrick] + end + + teardown do + Capybara.server = @old_capybara_server + end end diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb index c6a6aef92b..b078a5abc5 100644 --- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -28,6 +28,12 @@ class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome end end +class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox + test "uses selenium headless firefox" do + assert_equal :selenium, Capybara.current_driver + end +end + class SetHostTest < DrivenByRackTest test "sets default host" do assert_equal "http://127.0.0.1", Capybara.app_host diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 4673d7cc11..5a584b12e5 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -18,7 +18,7 @@ module ActionDispatch def test_filename_is_different_object file_str = "foo" uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new) - assert_not_equal file_str.object_id , uf.original_filename.object_id + assert_not_equal file_str.object_id, uf.original_filename.object_id end def test_filename_should_be_in_utf_8 @@ -100,14 +100,14 @@ module ActionDispatch def test_delegate_eof_to_tempfile tf = Class.new { def eof?; true end; } uf = Http::UploadedFile.new(tempfile: tf.new) - assert uf.eof? + assert_predicate uf, :eof? end def test_respond_to? tf = Class.new { def read; yield end } uf = Http::UploadedFile.new(tempfile: tf.new) - assert uf.respond_to?(:headers), "responds to headers" - assert uf.respond_to?(:read), "responds to read" + assert_respond_to uf, :headers + assert_respond_to uf, :read end end end diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb index 1e687acef2..b0622ac71a 100644 --- a/actionpack/test/journey/nodes/symbol_test.rb +++ b/actionpack/test/journey/nodes/symbol_test.rb @@ -8,10 +8,10 @@ module ActionDispatch class TestSymbol < ActiveSupport::TestCase def test_default_regexp? sym = Symbol.new "foo" - assert sym.default_regexp? + assert_predicate sym, :default_regexp? sym.regexp = nil - assert_not sym.default_regexp? + assert_not_predicate sym, :default_regexp? end end end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 29cc74471d..183f421bcf 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -30,7 +30,7 @@ module ActionDispatch def test_unicode get "/ほげ", to: "foo#bar" - #match the escaped version of /ほげ + # match the escaped version of /ほげ env = rails_env "PATH_INFO" => "/%E3%81%BB%E3%81%92" called = false router.recognize(env) do |r, params| diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb index 81ce07526f..d5c81a8421 100644 --- a/actionpack/test/journey/routes_test.rb +++ b/actionpack/test/journey/routes_test.rb @@ -17,11 +17,11 @@ module ActionDispatch def test_clear mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron" - assert_not_predicate routes, :empty? + assert_not_empty routes assert_equal 1, routes.length routes.clear - assert routes.empty? + assert_empty routes assert_equal 0, routes.length end @@ -43,7 +43,7 @@ module ActionDispatch mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron" assert_equal 1, @routes.anchored_routes.length - assert_predicate @routes.custom_routes, :empty? + assert_empty @routes.custom_routes mapper.get "/hello/:who", to: "foo#bar", as: "bar", who: /\d/ diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index c700cb72ec..ea7f8b205e 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,45 +1,3 @@ -* Fix issues with `field_error_proc` wrapping `optgroup` and select divider `option`. - Fixes #31088 - *Matthias Neumayr* - -* Remove deprecated Erubis ERB handler. - - *Rafael Mendonça França* - -* Remove default `alt` text generation. - - Fixes #30096 - - *Cameron Cundiff* - -* Add `srcset` option to `image_tag` helper. - - *Roberto Miranda* - -* Fix issues with scopes and engine on `current_page?` method. - - Fixes #29401. - - *Nikita Savrov* - -* Generate field ids in `collection_check_boxes` and `collection_radio_buttons`. - - This makes sure that the labels are linked up with the fields. - - Fixes #29014. - - *Yuji Yaginuma* - -* Add `:json` type to `auto_discovery_link_tag` to support [JSON Feeds](https://jsonfeed.org/version/1) - - *Mike Gunderloy* - -* Update `distance_of_time_in_words` helper to display better error messages - for bad input. - - *Jay Hayes* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index ac810e86d0..1cb3add0fc 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/README.rdoc b/actionview/README.rdoc index d5029599b7..03a0723564 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -11,7 +11,7 @@ The latest version of Action View can be installed with RubyGems: $ gem install actionview -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/actionview @@ -29,7 +29,7 @@ API documentation is at * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionview/RUNNING_UJS_TESTS.rdoc b/actionview/RUNNING_UJS_TESTS.rdoc index a575624a06..e30c2aee55 100644 --- a/actionview/RUNNING_UJS_TESTS.rdoc +++ b/actionview/RUNNING_UJS_TESTS.rdoc @@ -1,7 +1,8 @@ == Running UJS tests -Ensure that you can build the project and run tests. -Run rake ujs:server first, and then run the web tests by -visiting http://localhost:4567 in your browser. +Ensure that you can build the project by running: + rake ujs:server -rake ujs:server +Then run the web tests by visiting the following URL in your browser: + + http://localhost:4567 diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc index e99d5ca1df..4442dbdb9e 100644 --- a/actionview/RUNNING_UNIT_TESTS.rdoc +++ b/actionview/RUNNING_UNIT_TESTS.rdoc @@ -2,13 +2,13 @@ The easiest way to run the unit tests is through Rake. The default task runs the entire test suite for all classes. For more information, checkout the -full array of rake tasks with "rake -T" +full array of rake tasks with <tt>rake -T</tt> Rake can be found at https://ruby.github.io/rake/. == Running by hand -To run a single test suite +Run a single test suite: rake test TEST=path/to/test.rb @@ -18,10 +18,9 @@ which can be further narrowed down to one test: == Dependency on Active Record and database setup -Test cases in the test/activerecord/ directory depend on having -activerecord and sqlite3 installed. If Active Record is not in -actionview/../activerecord directory, or the sqlite3 rubygem is not installed, -these tests are skipped. - +Test cases in the +test/activerecord/+ directory depend on having +activerecord+ and +sqlite3+ installed. If Active Record is not in +actionview/../activerecord+ directory, or the +sqlite3+ Ruby gem is not installed, + these tests are skipped. Other tests are runnable from a fresh copy of actionview without any configuration. diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE index befcbdc7b7..28e1b12496 100644 --- a/actionview/app/assets/javascripts/MIT-LICENSE +++ b/actionview/app/assets/javascripts/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2017 Rails Core team +Copyright (c) 2007-2018 Rails Core team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md index 8198011b02..185dddc7e5 100644 --- a/actionview/app/assets/javascripts/README.md +++ b/actionview/app/assets/javascripts/README.md @@ -50,6 +50,6 @@ Run `bundle exec rake ujs:server` first, and then run the web tests by visiting rails-ujs is released under the [MIT License](MIT-LICENSE). -[data]: http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes "Embedding custom non-visible data with the data-* attributes" +[data]: https://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-attributes "Embedding custom non-visible data with the data-* attributes" [validator]: http://validator.w3.org/ [csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee index a2135c9851..a7eee52060 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee @@ -11,8 +11,18 @@ if typeof CustomEvent isnt 'function' evt = document.createEvent('CustomEvent') evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) evt + CustomEvent.prototype = window.Event.prototype + # Fix setting `defaultPrevented` when `preventDefault()` is called + # http://stackoverflow.com/questions/23349191/event-preventdefault-is-not-working-in-ie-11-for-custom-events + { preventDefault } = CustomEvent.prototype + CustomEvent.prototype.preventDefault = -> + result = preventDefault.call(this) + if @cancelable and not @defaultPrevented + Object.defineProperty(this, 'defaultPrevented', get: -> true) + result + # Triggers a custom event on an element and returns false if the event result is false # obj:: # a native DOM element diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 9533f8d56c..c1eeda75f5 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index dfd62bdcfd..1cf0bd3016 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -89,7 +89,7 @@ module ActionView end def digest(finder, stack = []) - Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") + ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") end def dependency_digest(finder, stack) diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index ed92490be7..77ae444a58 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -7,8 +7,8 @@ module ActionView end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index e362f13798..76b1c3fb6e 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -2,6 +2,8 @@ require "active_support/core_ext/array/extract_options" require "active_support/core_ext/hash/keys" +require "active_support/core_ext/object/inclusion" +require "active_support/core_ext/object/try" require "action_view/helpers/asset_url_helper" require "action_view/helpers/tag_helper" @@ -45,11 +47,11 @@ module ActionView # When the last parameter is a hash you can add HTML attributes using that # parameter. The following options are supported: # - # * <tt>:extname</tt> - Append an extension to the generated url unless the extension - # already exists. This only applies for relative urls. - # * <tt>:protocol</tt> - Sets the protocol of the generated url, this option only - # applies when a relative url and +host+ options are provided. - # * <tt>:host</tt> - When a relative url is provided the host is added to the + # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension + # already exists. This only applies for relative URLs. + # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only + # applies when a relative URL and +host+ options are provided. + # * <tt>:host</tt> - When a relative URL is provided the host is added to the # that path. # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline # when it is set to true. @@ -91,7 +93,7 @@ module ActionView content_tag("script".freeze, "", tag_options) }.join("\n").html_safe - request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) + request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request sources_tags end @@ -131,7 +133,7 @@ module ActionView sources_tags = sources.uniq.map { |source| href = path_to_stylesheet(source, path_options) - early_hints_links << "<#{href}>; rel=preload; as=stylesheet" + early_hints_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", @@ -140,7 +142,7 @@ module ActionView tag(:link, tag_options) }.join("\n").html_safe - request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) + request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request sources_tags end @@ -221,8 +223,69 @@ module ActionView }.merge!(options.symbolize_keys)) end + # Returns a link tag that browsers can use to preload the +source+. + # The +source+ can be the path of a resource managed by asset pipeline, + # a full path, or an URI. + # + # ==== Options + # + # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension. + # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type. + # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources. + # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+. + # + # ==== Examples + # + # preload_link_tag("custom_theme.css") + # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" /> + # + # preload_link_tag("/videos/video.webm") + # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" /> + # + # preload_link_tag(post_path(format: :json), as: "fetch") + # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" /> + # + # preload_link_tag("worker.js", as: "worker") + # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" /> + # + # preload_link_tag("//example.com/font.woff2") + # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/> + # + # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials") + # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" /> + # + # preload_link_tag("/media/audio.ogg", nopush: true) + # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" /> + # + def preload_link_tag(source, options = {}) + href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline)) + extname = File.extname(source).downcase.delete(".") + mime_type = options.delete(:type) || Template::Types[extname].try(:to_s) + as_type = options.delete(:as) || resolve_link_as(extname, mime_type) + crossorigin = options.delete(:crossorigin) + crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font") + nopush = options.delete(:nopush) || false + + link_tag = tag.link({ + rel: "preload", + href: href, + as: as_type, + type: mime_type, + crossorigin: crossorigin + }.merge!(options.symbolize_keys)) + + early_hints_link = "<#{href}>; rel=preload; as=#{as_type}" + early_hints_link += "; type=#{mime_type}" if mime_type + early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin + early_hints_link += "; nopush" if nopush + + request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request + + link_tag + end + # Returns an HTML image tag for the +source+. The +source+ can be a full - # path, a file or an Active Storage attachment. + # path, a file, or an Active Storage attachment. # # ==== Options # @@ -310,12 +373,13 @@ module ActionView # Returns an HTML video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The - # +sources+ can be full paths or files that exists in your public videos + # +sources+ can be full paths or files that exist in your public videos # directory. # # ==== Options - # You can add HTML attributes using the +options+. The +options+ supports - # two additional keys for convenience and conformance: + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. The following options are supported: # # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown # before the video loads. The path is calculated like the +src+ of +image_tag+. @@ -332,7 +396,7 @@ module ActionView # video_tag("trailer.ogg") # # => <video src="/videos/trailer.ogg"></video> # video_tag("trailer.ogg", controls: true, preload: 'none') - # # => <video preload="none" controls="controls" src="/videos/trailer.ogg" ></video> + # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true) @@ -359,9 +423,14 @@ module ActionView end end - # Returns an HTML audio tag for the +source+. - # The +source+ can be full path or file that exists in - # your public audios directory. + # Returns an HTML audio tag for the +sources+. If +sources+ is a string, + # a single audio tag will be returned. If +sources+ is an array, an audio + # tag with nested source tags for each source will be returned. The + # +sources+ can be full paths or files that exist in your public audios + # directory. + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. # # audio_tag("sound") # # => <audio src="/audios/sound"></audio> @@ -417,6 +486,18 @@ module ActionView raise ArgumentError, "Cannot pass a :size option with a :height or :width option" end end + + def resolve_link_as(extname, mime_type) + if extname == "js" + "script" + elsif extname == "css" + "style" + elsif extname == "vtt" + "track" + elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font)) + type + end + end end end end diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index a4dcfc9a6c..8cbe107e41 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -6,7 +6,7 @@ module ActionView # = Action View Asset URL Helpers module Helpers #:nodoc: # This module provides methods for generating asset paths and - # urls. + # URLs. # # image_path("rails.png") # # => "/assets/rails.png" @@ -57,8 +57,8 @@ module ActionView # You can read more about setting up your DNS CNAME records from your ISP. # # Note: This is purely a browser performance optimization and is not meant - # for server load balancing. See http://www.die.net/musings/page_load_time/ - # for background and http://www.browserscope.org/?category=network for + # for server load balancing. See https://www.die.net/musings/page_load_time/ + # for background and https://www.browserscope.org/?category=network for # connection limit data. # # Alternatively, you can exert more control over the asset host by setting @@ -97,7 +97,7 @@ module ActionView # still sending assets for plain HTTP requests from asset hosts. If you don't # have SSL certificates for each of the asset hosts this technique allows you # to avoid warnings in the client about mixed media. - # Note that the request parameter might not be supplied, e.g. when the assets + # Note that the +request+ parameter might not be supplied, e.g. when the assets # are precompiled via a Rake task. Make sure to use a +Proc+ instead of a lambda, # since a +Proc+ allows missing parameters and sets them to +nil+. # @@ -149,13 +149,13 @@ module ActionView # Below lists scenarios that apply to +asset_path+ whether or not you're # using the asset pipeline. # - # - All fully qualified urls are returned immediately. This bypasses the + # - All fully qualified URLs are returned immediately. This bypasses the # asset pipeline and all other behavior described. # # asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js" # # - All assets that begin with a forward slash are assumed to be full - # urls and will not be expanded. This will bypass the asset pipeline. + # URLs and will not be expanded. This will bypass the asset pipeline. # # asset_path("/foo.png") # => "/foo.png" # @@ -324,7 +324,7 @@ module ActionView # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js + # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/js/xmlhr.js # def javascript_url(source, options = {}) url_to_asset(source, { type: :javascript }.merge!(options)) @@ -351,7 +351,7 @@ module ActionView # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css + # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/assets/css/style.css # def stylesheet_url(source, options = {}) url_to_asset(source, { type: :stylesheet }.merge!(options)) @@ -381,7 +381,7 @@ module ActionView # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png + # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/assets/edit.png # def image_url(source, options = {}) url_to_asset(source, { type: :image }.merge!(options)) @@ -407,7 +407,7 @@ module ActionView # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi + # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/videos/hd.avi # def video_url(source, options = {}) url_to_asset(source, { type: :video }.merge!(options)) @@ -433,7 +433,7 @@ module ActionView # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav + # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/audios/horse.wav # def audio_url(source, options = {}) url_to_asset(source, { type: :audio }.merge!(options)) @@ -458,7 +458,7 @@ module ActionView # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf + # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/fonts/font.ttf # def font_url(source, options = {}) url_to_asset(source, { type: :font }.merge!(options)) diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 09040ccbc4..4c45f122fe 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -116,7 +116,7 @@ module ActionView when 10..19 then locale.t :less_than_x_seconds, count: 20 when 20..39 then locale.t :half_a_minute when 40..59 then locale.t :less_than_x_minutes, count: 1 - else locale.t :x_minutes, count: 1 + else locale.t :x_minutes, count: 1 end when 2...45 then locale.t :x_minutes, count: distance_in_minutes @@ -131,7 +131,7 @@ module ActionView when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round # 60 days up to 365 days when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round - else + else from_year = from_time.year from_year += 1 if from_time.month >= 3 to_year = to_time.year diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 6d2ace8cf8..15aa9ec2dd 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -19,7 +19,7 @@ module ActionView # compared to using vanilla HTML. # # Typically, a form designed to create or update a resource reflects the - # identity of the resource in several ways: (i) the url that the form is + # identity of the resource in several ways: (i) the URL that the form is # sent to (the form element's +action+ attribute) should result in a request # being routed to the appropriate controller action (with the appropriate <tt>:id</tt> # parameter in the case of an existing resource), (ii) input fields should @@ -166,7 +166,7 @@ module ActionView # So for example you may use a named route directly. When the model is # represented by a string or symbol, as in the example above, if the # <tt>:url</tt> option is not specified, by default the form will be - # sent back to the current url (We will describe below an alternative + # sent back to the current URL (We will describe below an alternative # resource-oriented usage of +form_for+ in which the URL does not need # to be specified explicitly). # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of @@ -478,6 +478,8 @@ module ActionView mattr_accessor :form_with_generates_remote_forms, default: true + mattr_accessor :form_with_generates_ids, default: false + # Creates a form tag based on mixing URLs, scopes, or models. # # # Using just a URL: @@ -606,7 +608,7 @@ module ActionView # This is helpful when fragment-caching the form. Remote forms # get the authenticity token from the <tt>meta</tt> tag, so embedding is # unnecessary unless you support browsers without JavaScript. - # * <tt>:local</tt> - By default form submits are remote and unobstrusive XHRs. + # * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs. # Disable remote submits with <tt>local: true</tt>. # * <tt>:skip_enforcing_utf8</tt> - By default a hidden field named +utf8+ # is output to enforce UTF-8 submits. Set to true to skip the field. @@ -640,16 +642,6 @@ module ActionView # # Where <tt>@document = Document.find(params[:id])</tt>. # - # When using labels +form_with+ requires setting the id on the field being - # labelled: - # - # <%= form_with(model: @post) do |form| %> - # <%= form.label :title %> - # <%= form.text_field :title, id: :post_title %> - # <% end %> - # - # See +label+ for more on how the +for+ attribute is derived. - # # === Mixing with other form helpers # # While +form_with+ uses a FormBuilder object it's possible to mix and @@ -746,7 +738,7 @@ module ActionView # end def form_with(model: nil, scope: nil, url: nil, format: nil, **options) options[:allow_method_names_outside_object] = true - options[:skip_default_ids] = true + options[:skip_default_ids] = !form_with_generates_ids if model url ||= polymorphic_path(model, format: format) @@ -1022,14 +1014,13 @@ module ActionView # <%= fields :comment do |fields| %> # <%= fields.text_field :body %> # <% end %> - # # => <input type="text" name="comment[body]> + # # => <input type="text" name="comment[body]"> # # # Using a model infers the scope and assigns field values: - # <%= fields model: Comment.new(body: "full bodied") do |fields| %< + # <%= fields model: Comment.new(body: "full bodied") do |fields| %> # <%= fields.text_field :body %> # <% end %> - # # => - # <input type="text" name="comment[body] value="full bodied"> + # # => <input type="text" name="comment[body]" value="full bodied"> # # # Using +fields+ with +form_with+: # <%= form_with model: @post do |form| %> @@ -1044,16 +1035,6 @@ module ActionView # or model is yielded, so any generated field names are prefixed with # either the passed scope or the scope inferred from the <tt>:model</tt>. # - # When using labels +fields+ requires setting the id on the field being - # labelled: - # - # <%= fields :comment do |fields| %> - # <%= fields.label :body %> - # <%= fields.text_field :body, id: :comment_body %> - # <% end %> - # - # See +label+ for more on how the +for+ attribute is derived. - # # === Mixing with other form helpers # # While +form_with+ uses a FormBuilder object it's possible to mix and @@ -1072,7 +1053,7 @@ module ActionView # FormOptionsHelper#collection_select and DateHelper#datetime_select. def fields(scope = nil, model: nil, **options, &block) options[:allow_method_names_outside_object] = true - options[:skip_default_ids] = true + options[:skip_default_ids] = !form_with_generates_ids if model scope ||= model_name_from_record_or_class(model).param_key @@ -1985,7 +1966,7 @@ module ActionView # See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method. def fields(scope = nil, model: nil, **options, &block) options[:allow_method_names_outside_object] = true - options[:skip_default_ids] = true + options[:skip_default_ids] = !FormHelper.form_with_generates_ids convert_to_legacy_options(options) diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 02a44477c1..fe5e0b693e 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -214,9 +214,13 @@ module ActionView # * +method+ - The attribute of +object+ corresponding to the select tag # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an - # array of child objects representing the <tt><option></tt> tags. + # array of child objects representing the <tt><option></tt> tags. It can also be any object that responds + # to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the + # value. # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a - # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. + # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object + # that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to + # retrieve the label. # * +option_key_method+ - The name of a method which, when called on a child object of a member of # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag. # * +option_value_method+ - The name of a method which, when called on a child object of a member of @@ -457,9 +461,9 @@ module ActionView def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) collection.map do |group| option_tags = options_from_collection_for_select( - group.send(group_method), option_key_method, option_value_method, selected_key) + value_for_collection(group, group_method), option_key_method, option_value_method, selected_key) - content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method)) + content_tag("optgroup".freeze, option_tags, label: value_for_collection(group, group_label_method)) end.join.html_safe end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index 8934a9894c..fed908fcdb 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -97,7 +97,7 @@ module ActionView index = name_and_id_index(options) options["name"] = options.fetch("name") { tag_name(options["multiple"], index) } - unless skip_default_ids? + if generate_ids? options["id"] = options.fetch("id") { tag_id(index) } if namespace = options.delete("namespace") options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace @@ -183,8 +183,8 @@ module ActionView end end - def skip_default_ids? - @skip_default_ids + def generate_ids? + !@skip_default_ids end end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index 56b48bbd62..02bd099784 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -75,10 +75,6 @@ module ActionView def render_component(builder) builder.translation end - - def skip_default_ids? - false # The id is used as the `for` attribute. - end end end end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 84d38aa416..34138de00e 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -13,9 +13,9 @@ module ActionView # # ==== Sanitization # - # Most text helpers by default sanitize the given content, but do not escape it. - # This means HTML tags will appear in the page but all malicious code will be removed. - # Let's look at some examples using the +simple_format+ method: + # Most text helpers that generate HTML output sanitize the given input by default, + # but do not escape it. This means HTML tags will appear in the page but all malicious + # code will be removed. Let's look at some examples using the +simple_format+ method: # # simple_format('<a href="http://example.com/">Example</a>') # # => "<p><a href=\"http://example.com/\">Example</a></p>" @@ -128,7 +128,7 @@ module ActionView # # => You searched for: <a href="search?q=rails">rails</a> # # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false) - # # => "<a>ruby</a> on <mark>rails</mark>" + # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark> def highlight(text, phrases, options = {}) text = sanitize(text) if options.fetch(:sanitize, true) diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 9900e0cd03..889562c478 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -139,6 +139,11 @@ module ActionView # link_to "Profiles", controller: "profiles" # # => <a href="/profiles">Profiles</a> # + # When name is +nil+ the href is presented instead + # + # link_to nil, "http://example.com" + # # => <a href="http://www.example.com">http://www.example.com</a> + # # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: # # <%= link_to(@profile) do %> @@ -589,7 +594,7 @@ module ActionView end def add_method_to_attributes!(html_options, method) - if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ + if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/ if html_options["rel"].blank? html_options["rel"] = "nofollow" else @@ -599,6 +604,19 @@ module ActionView html_options["data-method"] = method end + STRINGIFIED_COMMON_METHODS = { + get: "get", + delete: "delete", + patch: "patch", + post: "post", + put: "put", + }.freeze + + def method_not_get_method?(method) + return false unless method + (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get" + end + def token_tag(token = nil, form_options: {}) if token != false && protect_against_forgery? token ||= form_authenticity_token(form_options: form_options) diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index b22347c55c..73dfb267bb 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -22,8 +22,15 @@ module ActionView initializer "action_view.form_with_generates_remote_forms" do |app| ActiveSupport.on_load(:action_view) do form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms) - unless form_with_generates_remote_forms.nil? - ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms + end + end + + initializer "action_view.form_with_generates_ids" do |app| + ActiveSupport.on_load(:action_view) do + form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids) + unless form_with_generates_ids.nil? + ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids end end end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index ca49eb1144..276a28ce07 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -65,7 +65,9 @@ module ActionView yielder = lambda { |*name| view._layout_for(*name) } instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do + outer_config = I18n.config fiber = Fiber.new do + I18n.config = outer_config if layout layout.render(view, locals, output, &yielder) else diff --git a/actionview/package.json b/actionview/package.json index 4cbf0207e5..624eb5de93 100644 --- a/actionview/package.json +++ b/actionview/package.json @@ -1,6 +1,6 @@ { "name": "rails-ujs", - "version": "5.2.0-alpha", + "version": "6.0.0-alpha", "description": "Ruby on Rails unobtrusive scripting adapter", "main": "lib/assets/compiled/rails-ujs.js", "files": [ diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 9df2a73448..8a9d7982d3 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base end module Quiz - #Models + # Models Question = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index b39ecd8813..7f48b515a0 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -38,7 +38,7 @@ class ActiveRecordTestConnector end rescue Exception => e # errors from ActiveRecord setup $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" - #$stderr.puts " #{e.backtrace.join("\n ")}\n" + # $stderr.puts " #{e.backtrace.join("\n ")}\n" self.able_to_connect = false end diff --git a/actionview/test/fixtures/layouts/streaming_with_locale.erb b/actionview/test/fixtures/layouts/streaming_with_locale.erb new file mode 100644 index 0000000000..e1fdad2073 --- /dev/null +++ b/actionview/test/fixtures/layouts/streaming_with_locale.erb @@ -0,0 +1,2 @@ +layout.locale: <%= I18n.locale %> +<%= yield %> diff --git a/actionview/test/fixtures/test/streaming_with_locale.erb b/actionview/test/fixtures/test/streaming_with_locale.erb new file mode 100644 index 0000000000..b0f2b2f7e9 --- /dev/null +++ b/actionview/test/fixtures/test/streaming_with_locale.erb @@ -0,0 +1 @@ +view.locale: <%= I18n.locale %> diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 7475f5cc3c..6d98eacfb8 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -214,6 +214,17 @@ class AssetTagHelperTest < ActionView::TestCase %(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />) } + PreloadLinkToTag = { + %(preload_link_tag '/styles/custom_theme.css') => %(<link rel="preload" href="/styles/custom_theme.css" as="style" type="text/css" />), + %(preload_link_tag '/videos/video.webm') => %(<link rel="preload" href="/videos/video.webm" as="video" type="video/webm" />), + %(preload_link_tag '/posts.json', as: 'fetch') => %(<link rel="preload" href="/posts.json" as="fetch" type="application/json" />), + %(preload_link_tag '/users', as: 'fetch', type: 'application/json') => %(<link rel="preload" href="/users" as="fetch" type="application/json" />), + %(preload_link_tag '//example.com/map?callback=initMap', as: 'fetch', type: 'application/javascript') => %(<link rel="preload" href="//example.com/map?callback=initMap" as="fetch" type="application/javascript" />), + %(preload_link_tag '//example.com/font.woff2') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>), + %(preload_link_tag '//example.com/font.woff2', crossorigin: 'use-credentials') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />), + %(preload_link_tag '/media/audio.ogg', nopush: true) => %(<link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />) + } + VideoPathToTag = { %(video_path("xml")) => %(/videos/xml), %(video_path("xml.ogg")) => %(/videos/xml.ogg), @@ -396,7 +407,7 @@ class AssetTagHelperTest < ActionView::TestCase end def test_javascript_include_tag_is_html_safe - assert javascript_include_tag("prototype").html_safe? + assert_predicate javascript_include_tag("prototype"), :html_safe? end def test_javascript_include_tag_relative_protocol @@ -440,9 +451,17 @@ class AssetTagHelperTest < ActionView::TestCase } end + def test_stylesheet_link_tag_without_request + @request = nil + assert_dom_equal( + %(<link rel="stylesheet" media="screen" href="/stylesheets/foo.css" />), + stylesheet_link_tag("foo.css") + ) + end + def test_stylesheet_link_tag_is_html_safe - assert stylesheet_link_tag("dir/file").html_safe? - assert stylesheet_link_tag("dir/other/file", "dir/file2").html_safe? + assert_predicate stylesheet_link_tag("dir/file"), :html_safe? + assert_predicate stylesheet_link_tag("dir/other/file", "dir/file2"), :html_safe? end def test_stylesheet_link_tag_escapes_options @@ -464,6 +483,11 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("wellington") end + def test_javascript_include_tag_without_request + @request = nil + assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js") + end + def test_image_path ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -522,6 +546,10 @@ class AssetTagHelperTest < ActionView::TestCase FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end + def test_preload_link_tag + PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + def test_video_path VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -778,6 +806,23 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase end end +class AssetTagHelperWithoutRequestTest < ActionView::TestCase + tests ActionView::Helpers::AssetTagHelper + + undef :request + + def test_stylesheet_link_tag_without_request + assert_dom_equal( + %(<link rel="stylesheet" media="screen" href="/stylesheets/foo.css" />), + stylesheet_link_tag("foo.css") + ) + end + + def test_javascript_include_tag_without_request + assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js") + end +end + class AssetUrlHelperControllerTest < ActionView::TestCase tests ActionView::Helpers::AssetUrlHelper diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 1be20dcaae..8e683cb48a 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -257,7 +257,7 @@ class AtomFeedTest < ActionController::TestCase get :index, params: { id: "provide_builder" } # because we pass in the non-default builder, the content generated by the # helper should go 'nowhere'. Leaving the response body blank. - assert @response.body.blank? + assert_predicate @response.body, :blank? end end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 8a1c00fd00..31c280a91c 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -155,12 +155,12 @@ class CaptureHelperTest < ActionView::TestCase content_for :title do content_tag(:p, "title") end - assert content_for(:title).html_safe? + assert_predicate content_for(:title), :html_safe? content_for :title, "", flush: true content_for(:title) do content_tag(:p, "title") end - assert content_for(:title).html_safe? + assert_predicate content_for(:title), :html_safe? end def test_provide diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 5a5550438b..79e52ccc47 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -687,7 +687,7 @@ class DateHelperTest < ActionView::TestCase 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" - assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true , minute_step: 15) + assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true, minute_step: 15) end def test_select_minute_nil_with_blank @@ -703,7 +703,7 @@ class DateHelperTest < ActionView::TestCase 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" - assert_dom_equal expected, select_minute(nil, include_blank: true , minute_step: 15) + assert_dom_equal expected, select_minute(nil, include_blank: true, minute_step: 15) end def test_select_minute_with_hidden @@ -3593,25 +3593,25 @@ class DateHelperTest < ActionView::TestCase end def test_select_html_safety - assert select_day(16).html_safe? - assert select_month(8).html_safe? - assert select_year(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe? - assert select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe? - assert select_second(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe? + assert_predicate select_day(16), :html_safe? + assert_predicate select_month(8), :html_safe? + assert_predicate select_year(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe? + assert_predicate select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe? + assert_predicate select_second(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe? - assert select_minute(8, use_hidden: true).html_safe? - assert select_month(8, prompt: "Choose month").html_safe? + assert_predicate select_minute(8, use_hidden: true), :html_safe? + assert_predicate select_month(8, prompt: "Choose month"), :html_safe? - assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector" }).html_safe? - assert select_date(Time.mktime(2003, 8, 16), date_separator: " / ", start_year: 2003, end_year: 2005, prefix: "date[first]").html_safe? + assert_predicate select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector" }), :html_safe? + assert_predicate select_date(Time.mktime(2003, 8, 16), date_separator: " / ", start_year: 2003, end_year: 2005, prefix: "date[first]"), :html_safe? end def test_object_select_html_safety @post = Post.new @post.written_on = Date.new(2004, 6, 15) - assert date_select("post", "written_on", default: Time.local(2006, 9, 19, 15, 16, 35), include_blank: true).html_safe? - assert time_select("post", "written_on", ignore_date: true).html_safe? + assert_predicate date_select("post", "written_on", default: Time.local(2006, 9, 19, 15, 16, 35), include_blank: true), :html_safe? + assert_predicate time_select("post", "written_on", ignore_date: true), :html_safe? end def test_time_tag_with_date diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb index 8b804105f4..bd702dbe94 100644 --- a/actionview/test/template/erb_util_test.rb +++ b/actionview/test/template/erb_util_test.rb @@ -70,24 +70,24 @@ class ErbUtilTest < ActiveSupport::TestCase def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings value = json_escape("asdf") - assert !value.html_safe? + assert_not_predicate value, :html_safe? end def test_json_escape_returns_safe_strings_when_passed_safe_strings value = json_escape("asdf".html_safe) - assert value.html_safe? + assert_predicate value, :html_safe? end def test_html_escape_is_html_safe escaped = h("<p>") assert_equal "<p>", escaped - assert escaped.html_safe? + assert_predicate escaped, :html_safe? end def test_html_escape_passes_html_escape_unmodified escaped = h("<p>".html_safe) assert_equal "<p>", escaped - assert escaped.html_safe? + assert_predicate escaped, :html_safe? end def test_rest_in_ascii @@ -104,11 +104,11 @@ class ErbUtilTest < ActiveSupport::TestCase def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings value = html_escape_once("1 < 2 & 3") - assert !value.html_safe? + assert_not_predicate value, :html_safe? end def test_html_escape_once_returns_safe_strings_when_passed_safe_strings value = html_escape_once("1 < 2 & 3".html_safe) - assert value.html_safe? + assert_predicate value, :html_safe? end end diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index c7d49070ce..0d224d0c46 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -5,6 +5,15 @@ require "controller/fake_models" class FormWithTest < ActionView::TestCase include RenderERBUtils + + setup do + @old_value = ActionView::Helpers::FormHelper.form_with_generates_ids + ActionView::Helpers::FormHelper.form_with_generates_ids = true + end + + teardown do + ActionView::Helpers::FormHelper.form_with_generates_ids = @old_value + end end class FormWithActsLikeFormTagTest < FormWithTest @@ -99,7 +108,7 @@ class FormWithActsLikeFormTagTest < FormWithTest actual = form_with(skip_enforcing_utf8: true) expected = whole_form("http://www.example.com", skip_enforcing_utf8: true) assert_dom_equal expected, actual - assert actual.html_safe? + assert_predicate actual, :html_safe? end def test_form_with_with_block_in_erb @@ -218,7 +227,7 @@ class FormWithActsLikeFormForTest < FormWithTest @post = Post.new @comment = Comment.new - def @post.errors() + def @post.errors Class.new { def [](field); field == "author_name" ? ["can't be empty"] : [] end def empty?() false end @@ -314,17 +323,45 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts/123", "create-post", method: "patch") do "<label for='post_title'>The Title</label>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" \ + "<select name='post[category]' id='post_category'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \ + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \ + "<button name='button' type='submit'>Create post</button>" \ + "<button name='button' type='submit'><span>Create post</span></button>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_not_outputting_ids + old_value = ActionView::Helpers::FormHelper.form_with_generates_ids + ActionView::Helpers::FormHelper.form_with_generates_ids = false + + form_with(model: @post, id: "create-post") do |f| + concat f.label(:title) { "The Title" } + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.select(:category, %w( animal economy sports )) + concat f.submit("Create post") + end + + expected = whole_form("/posts/123", "create-post", method: "patch") do + "<label>The Title</label>" \ "<input name='post[title]' type='text' value='Hello World' />" \ "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" \ "<select name='post[category]'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \ - "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \ - "<button name='button' type='submit'>Create post</button>" \ - "<button name='button' type='submit'><span>Create post</span></button>" + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end assert_dom_equal expected, output_buffer + ensure + ActionView::Helpers::FormHelper.form_with_generates_ids = old_value end def test_form_with_only_url_on_create @@ -335,7 +372,7 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do '<label for="title">Label me</label>' \ - '<input type="text" name="title">' + '<input type="text" name="title" id="title">' end assert_dom_equal expected, output_buffer @@ -349,7 +386,7 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts/123") do '<label for="title">Label me</label>' \ - '<input type="text" name="title">' + '<input type="text" name="title" id="title">' end assert_dom_equal expected, output_buffer @@ -361,7 +398,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123") do - '<input type="text" name="no_model_to_back_this_badboy">' + '<input type="text" name="no_model_to_back_this_badboy" id="no_model_to_back_this_badboy" >' end assert_dom_equal expected, output_buffer @@ -373,7 +410,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: :patch) do - '<input type="text" name="post[this_dont_exist_on_post]">' + '<input type="text" name="post[this_dont_exist_on_post]" id="post_this_dont_exist_on_post" >' end assert_dom_equal expected, output_buffer @@ -391,8 +428,8 @@ class FormWithActsLikeFormForTest < FormWithTest end.new form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f| - assert_dom_equal '<input type="hidden" name="other_name[private_property]">', f.hidden_field(:private_property) - assert_dom_equal '<input type="hidden" name="other_name[protected_property]">', f.hidden_field(:protected_property) + assert_dom_equal '<input type="hidden" name="other_name[private_property]" id="other_name_private_property">', f.hidden_field(:private_property) + assert_dom_equal '<input type="hidden" name="other_name[protected_property]" id="other_name_protected_property">', f.hidden_field(:protected_property) end end @@ -459,7 +496,7 @@ class FormWithActsLikeFormForTest < FormWithTest "<label for='post_active_false'>" \ "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "false</label>" \ - "<input name='post[id]' type='hidden' value='1' />" + "<input name='post[id]' type='hidden' value='1' id='post_id' />" end assert_dom_equal expected, output_buffer @@ -557,7 +594,7 @@ class FormWithActsLikeFormForTest < FormWithTest "<label for='post_tag_ids_3'>" \ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "Tag 3</label>" \ - "<input name='post[id]' type='hidden' value='1' />" + "<input name='post[id]' type='hidden' value='1' id='post_id' />" end assert_dom_equal expected, output_buffer @@ -587,7 +624,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch", multipart: true) do - "<input name='post[file]' type='file' />" + "<input name='post[file]' type='file' id='post_file' />" end assert_dom_equal expected, output_buffer @@ -601,7 +638,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch", multipart: true) do - "<input name='post[comment][file]' type='file' />" + "<input name='post[comment][file]' type='file' id='post_comment_file'/>" end assert_dom_equal expected, output_buffer @@ -640,7 +677,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/44", method: "patch") do - "<input name='post[title]' type='text' value='And his name will be forty and four.' />" \ + "<input name='post[title]' type='text' value='And his name will be forty and four.' id='post_title' />" \ "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" end @@ -658,10 +695,10 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts/123", "create-post", method: "patch") do "<label for='other_name_title' class='post_title'>Title</label>" \ - "<input name='other_name[title]' value='Hello World' type='text' />" \ - "<textarea name='other_name[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='other_name[title]' value='Hello World' type='text' id='other_name_title' />" \ + "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='other_name[secret]' value='0' type='hidden' />" \ - "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' />" \ + "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' id='other_name_secret' />" \ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" end @@ -676,10 +713,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "delete") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret'/>" end assert_dom_equal expected, output_buffer @@ -693,10 +730,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "delete") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" end assert_dom_equal expected, output_buffer @@ -710,7 +747,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/search", "search-post", method: "get") do - "<input name='post[title]' type='search' />" + "<input name='post[title]' type='search' id='post_title' />" end assert_dom_equal expected, output_buffer @@ -724,10 +761,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" end assert_dom_equal expected, output_buffer @@ -744,10 +781,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "patch", local: true) do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" end assert_dom_equal expected, output_buffer @@ -761,7 +798,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", skip_enforcing_utf8: true) do - "<input name='post[title]' type='text' value='Hello World' />" + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" end assert_dom_equal expected, output_buffer @@ -773,7 +810,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", skip_enforcing_utf8: false) do - "<input name='post[title]' type='text' value='Hello World' />" + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" end assert_dom_equal expected, output_buffer @@ -787,10 +824,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" end assert_dom_equal expected, output_buffer @@ -806,10 +843,10 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts/123", method: "patch") do "<label for='post_123_title'>Title</label>" \ - "<input name='post[123][title]' type='text' value='Hello World' />" \ - "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[123][title]' type='text' value='Hello World' id='post_123_title' />" \ + "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[123][secret]' type='hidden' value='0' />" \ - "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' id='post_123_secret' />" end assert_dom_equal expected, output_buffer @@ -823,10 +860,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[][title]' type='text' value='Hello World' />" \ - "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[][title]' type='text' value='Hello World' id='post__title' />" \ + "<textarea name='post[][body]' id='post__body' >\nBack to the hill and over it again!</textarea>" \ "<input name='post[][secret]' type='hidden' value='0' />" \ - "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[][secret]' checked='checked' type='checkbox' value='1' id='post__secret' />" end assert_dom_equal expected, output_buffer @@ -841,7 +878,7 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts/123", method: "patch") do "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \ - "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" \ + "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' id='post_author_name' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -859,7 +896,7 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts/123", method: "patch") do "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \ - "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" \ + "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' id='post_author_name' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -947,7 +984,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: :patch) do - '<input type="text" name="post[comment][dont_exist_on_model]">' + '<input type="text" name="post[comment][dont_exist_on_model]" id="post_comment_dont_exist_on_model" >' end assert_dom_equal expected, output_buffer @@ -967,7 +1004,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form do - '<input name="posts[post][0][comment][1][dont_exist_on_model]" type="text">' + '<input name="posts[post][0][comment][1][dont_exist_on_model]" type="text" id="posts_post_0_comment_1_dont_exist_on_model" >' end assert_dom_equal expected, output_buffer @@ -982,7 +1019,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[comment][body]' type='text' value='Hello World' />" + "<input name='post[comment][body]' type='text' value='Hello World' id='post_comment_body' />" end assert_dom_equal expected, output_buffer @@ -1002,7 +1039,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form do - "<input name='posts[post][0][comment][1][name]' type='text' value='comment #1' />" + "<input name='posts[post][0][comment][1][name]' type='text' value='comment #1' id='posts_post_0_comment_1_name' />" end assert_dom_equal expected, output_buffer @@ -1017,8 +1054,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[123][title]' type='text' value='Hello World' />" \ - "<input name='post[123][comment][][name]' type='text' value='new comment' />" + "<input name='post[123][title]' type='text' value='Hello World' id='post_123_title' />" \ + "<input name='post[123][comment][][name]' type='text' value='new comment' id='post_123_comment__name' />" end assert_dom_equal expected, output_buffer @@ -1033,8 +1070,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[1][title]' type='text' value='Hello World' />" \ - "<input name='post[1][comment][1][name]' type='text' value='new comment' />" + "<input name='post[1][title]' type='text' value='Hello World' id='post_1_title' />" \ + "<input name='post[1][comment][1][name]' type='text' value='new comment' id='post_1_comment_1_name' />" end assert_dom_equal expected, output_buffer @@ -1048,7 +1085,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[1][comment][title]' type='text' value='Hello World' />" + "<input name='post[1][comment][title]' type='text' value='Hello World' id='post_1_comment_title' />" end assert_dom_equal expected, output_buffer @@ -1062,7 +1099,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[1][comment][5][title]' type='text' value='Hello World' />" + "<input name='post[1][comment][5][title]' type='text' value='Hello World' id='post_1_comment_5_title' />" end assert_dom_equal expected, output_buffer @@ -1076,7 +1113,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[123][comment][title]' type='text' value='Hello World' />" + "<input name='post[123][comment][title]' type='text' value='Hello World' id='post_123_comment_title' />" end assert_dom_equal expected, output_buffer @@ -1090,7 +1127,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[comment][5][title]' type='radio' value='hello' />" + "<input name='post[comment][5][title]' type='radio' value='hello' id='post_comment_5_title_hello' />" end assert_dom_equal expected, output_buffer @@ -1104,7 +1141,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[123][comment][123][title]' type='text' value='Hello World' />" + "<input name='post[123][comment][123][title]' type='text' value='Hello World' id='post_123_comment_123_title' />" end assert_dom_equal expected, output_buffer @@ -1124,9 +1161,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[123][comment][5][title]' type='text' value='Hello World' />" + "<input name='post[123][comment][5][title]' type='text' value='Hello World' id='post_123_comment_5_title' />" end + whole_form("/posts/123", method: "patch") do - "<input name='post[1][comment][123][title]' type='text' value='Hello World' />" + "<input name='post[1][comment][123][title]' type='text' value='Hello World' id='post_1_comment_123_title' />" end assert_dom_equal expected, output_buffer @@ -1143,8 +1180,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="new author" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="new author" id="post_author_attributes_name" />' end assert_dom_equal expected, output_buffer @@ -1170,9 +1207,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ - '<input name="post[author_attributes][id]" type="hidden" value="321" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' end assert_dom_equal expected, output_buffer @@ -1189,9 +1226,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ - '<input name="post[author_attributes][id]" type="hidden" value="321" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' end assert_dom_equal expected, output_buffer @@ -1208,8 +1245,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' end assert_dom_equal expected, output_buffer @@ -1226,8 +1263,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' end assert_dom_equal expected, output_buffer @@ -1244,9 +1281,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ - '<input name="post[author_attributes][id]" type="hidden" value="321" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' end assert_dom_equal expected, output_buffer @@ -1264,9 +1301,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][id]" type="hidden" value="321" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' end assert_dom_equal expected, output_buffer @@ -1285,11 +1322,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' end assert_dom_equal expected, output_buffer @@ -1312,11 +1349,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ - '<input name="post[author_attributes][id]" type="hidden" value="321" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1339,10 +1376,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1365,11 +1402,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ - '<input name="post[author_attributes][id]" type="hidden" value="321" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1388,11 +1425,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' end assert_dom_equal expected, output_buffer @@ -1412,11 +1449,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1435,9 +1472,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="new comment" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="new comment" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1456,10 +1493,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id"/>' \ + '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1474,7 +1511,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' end assert_dom_equal expected, output_buffer @@ -1491,11 +1528,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' end assert_dom_equal expected, output_buffer @@ -1512,11 +1549,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' end assert_dom_equal expected, output_buffer @@ -1547,11 +1584,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' end assert_dom_equal expected, output_buffer @@ -1570,10 +1607,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' \ - '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ - '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />' + '<input name="post[title]" type="text" value="Hello World" id="post_title" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />' end assert_dom_equal expected, output_buffer @@ -1590,8 +1627,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ - '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" id="post_comments_attributes_abc_name" />' \ + '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />' end assert_dom_equal expected, output_buffer @@ -1607,8 +1644,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ - '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" id="post_comments_attributes_abc_name" />' \ + '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />' end assert_dom_equal expected, output_buffer @@ -1630,8 +1667,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ - '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" id="post_comments_attributes_abc_name" />' \ + '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />' end assert_dom_equal expected, output_buffer @@ -1716,18 +1753,18 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ - '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' \ - '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' \ - '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ - '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" />' \ - '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' \ - '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' \ - '<input name="post[tags_attributes][0][id]" type="hidden" value="123" />' \ - '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" />' \ - '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' \ - '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' \ - '<input name="post[tags_attributes][1][id]" type="hidden" value="456" />' + '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \ + '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" id="post_comments_attributes_0_relevances_attributes_0_value" />' \ + '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" id="post_comments_attributes_0_relevances_attributes_0_id"/>' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id"/>' \ + '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" id="post_tags_attributes_0_value"/>' \ + '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" id="post_tags_attributes_0_relevances_attributes_0_value"/>' \ + '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" id="post_tags_attributes_0_relevances_attributes_0_id"/>' \ + '<input name="post[tags_attributes][0][id]" type="hidden" value="123" id="post_tags_attributes_0_id"/>' \ + '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" id="post_tags_attributes_1_value"/>' \ + '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" id="post_tags_attributes_1_relevances_attributes_0_value"/>' \ + '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" id="post_tags_attributes_1_relevances_attributes_0_id"/>' \ + '<input name="post[tags_attributes][1][id]" type="hidden" value="456" id="post_tags_attributes_1_id"/>' end assert_dom_equal expected, output_buffer @@ -1743,7 +1780,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[author_attributes][name]" type="text" value="hash backed author" />' + '<input name="post[author_attributes][name]" type="text" value="hash backed author" id="post_author_attributes_name" />' end assert_dom_equal expected, output_buffer @@ -1757,10 +1794,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" assert_dom_equal expected, output_buffer end @@ -1773,10 +1810,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[123][title]' type='text' value='Hello World' />" \ - "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[123][title]' type='text' value='Hello World' id='post_123_title' />" \ + "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[123][secret]' type='hidden' value='0' />" \ - "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' id='post_123_secret' />" assert_dom_equal expected, output_buffer end @@ -1789,10 +1826,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[][title]' type='text' value='Hello World' />" \ - "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[][title]' type='text' value='Hello World' id='post__title' />" \ + "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[][secret]' type='hidden' value='0' />" \ - "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[][secret]' checked='checked' type='checkbox' value='1' id='post__secret' />" assert_dom_equal expected, output_buffer end @@ -1805,10 +1842,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[abc][title]' type='text' value='Hello World' />" \ - "<textarea name='post[abc][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[abc][title]' type='text' value='Hello World' id='post_abc_title' />" \ + "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[abc][secret]' type='hidden' value='0' />" \ - "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' id='post_abc_secret' />" assert_dom_equal expected, output_buffer end @@ -1821,10 +1858,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" assert_dom_equal expected, output_buffer end @@ -1837,10 +1874,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \ "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" assert_dom_equal expected, output_buffer end @@ -1852,7 +1889,7 @@ class FormWithActsLikeFormForTest < FormWithTest end assert_dom_equal "<label for=\"author_post_title\">Title</label>" \ - "<input name='author[post][title]' type='text' value='Hello World' />", + "<input name='author[post][title]' type='text' value='Hello World' id='author_post_title' id='author_post_1_title' />", output_buffer end @@ -1863,7 +1900,7 @@ class FormWithActsLikeFormForTest < FormWithTest end assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \ - "<input name='author[post][1][title]' type='text' value='Hello World' />", + "<input name='author[post][1][title]' type='text' value='Hello World' id='author_post_1_title' />", output_buffer end @@ -1882,10 +1919,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \ "<input name='parent_post[secret]' type='hidden' value='0' />" \ - "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' />" + "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' id='parent_post_secret' />" end assert_dom_equal expected, output_buffer @@ -1902,9 +1939,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ - "<input name='post[comment][name]' type='text' value='new comment' />" + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[comment][name]' type='text' value='new comment' id='post_comment_name' />" end assert_dom_equal expected, output_buffer @@ -1918,7 +1955,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[category][name]' type='text' />" + "<input name='post[category][name]' type='text' id='post_category_name' />" end assert_dom_equal expected, output_buffer @@ -1942,9 +1979,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \ - "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \ - "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>" + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title'/><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \ + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>" end assert_dom_equal expected, output_buffer @@ -1961,9 +1998,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \ - "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \ - "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>" + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \ + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>" end assert_dom_equal expected, output_buffer @@ -1980,7 +2017,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>" end assert_dom_equal expected, output_buffer @@ -1995,7 +2032,7 @@ class FormWithActsLikeFormForTest < FormWithTest concat f.text_field(:title) end - expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>" assert_dom_equal expected, output_buffer end @@ -2007,7 +2044,7 @@ class FormWithActsLikeFormForTest < FormWithTest concat f.text_field(:title) end - expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title' /><br/>" assert_dom_equal expected, output_buffer end @@ -2020,9 +2057,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \ - "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \ - "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>" + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' id='post_title'/><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \ + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>" assert_dom_equal expected, output_buffer end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index ac64096908..b8fad090c5 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -105,7 +105,7 @@ class FormHelperTest < ActionView::TestCase @post = Post.new @comment = Comment.new - def @post.errors() + def @post.errors Class.new { def [](field); field == "author_name" ? ["can't be empty"] : [] end def empty?() false end @@ -612,7 +612,7 @@ class FormHelperTest < ActionView::TestCase end def test_check_box_is_html_safe - assert check_box("post", "secret").html_safe? + assert_predicate check_box("post", "secret"), :html_safe? end def test_check_box_checked_if_object_value_is_same_that_check_value @@ -747,19 +747,19 @@ class FormHelperTest < ActionView::TestCase end def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal - @post.secret = BigDecimal.new(0) + @post.secret = BigDecimal(0) assert_dom_equal( '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', check_box("post", "secret", {}, 0, 1) ) - @post.secret = BigDecimal.new(1) + @post.secret = BigDecimal(1) assert_dom_equal( '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', check_box("post", "secret", {}, 0, 1) ) - @post.secret = BigDecimal.new(2.2, 1) + @post.secret = BigDecimal(2.2, 1) assert_dom_equal( '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', check_box("post", "secret", {}, 0, 1) @@ -775,7 +775,7 @@ class FormHelperTest < ActionView::TestCase end def test_check_box_with_nil_unchecked_value_is_html_safe - assert check_box("post", "secret", {}, "on", nil).html_safe? + assert_predicate check_box("post", "secret", {}, "on", nil), :html_safe? end def test_check_box_with_multiple_behavior @@ -1560,6 +1560,38 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_is_not_affected_by_form_with_generates_ids + old_value = ActionView::Helpers::FormHelper.form_with_generates_ids + ActionView::Helpers::FormHelper.form_with_generates_ids = false + + form_for(@post, html: { id: "create-post" }) do |f| + concat f.label(:title) { "The Title" } + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit("Create post") + concat f.button("Create post") + concat f.button { + concat content_tag(:span, "Create post") + } + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + "<label for='post_title'>The Title</label>" \ + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" \ + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \ + "<button name='button' type='submit'>Create post</button>" \ + "<button name='button' type='submit'><span>Create post</span></button>" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Helpers::FormHelper.form_with_generates_ids = old_value + end + def test_form_for_with_collection_radio_buttons post = Post.new def post.active; false; end diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index f0eed1e290..f82eada869 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -337,8 +337,24 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_option_groups_from_collection_for_select_with_callable_group_method + group_proc = Proc.new { |c| c.countries } + assert_dom_equal( + "<optgroup label=\"<Africa>\"><option value=\"<sa>\"><South Africa></option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>", + option_groups_from_collection_for_select(dummy_continents, group_proc, "continent_name", "country_id", "country_name", "dk") + ) + end + + def test_option_groups_from_collection_for_select_with_callable_group_label_method + label_proc = Proc.new { |c| c.continent_name } + assert_dom_equal( + "<optgroup label=\"<Africa>\"><option value=\"<sa>\"><South Africa></option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>", + option_groups_from_collection_for_select(dummy_continents, "countries", label_proc, "country_id", "country_name", "dk") + ) + end + def test_option_groups_from_collection_for_select_returns_html_safe_string - assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe? + assert_predicate option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk"), :html_safe? end def test_grouped_options_for_select_with_array @@ -367,7 +383,7 @@ class FormOptionsHelperTest < ActionView::TestCase assert_dom_equal( "<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>", - grouped_options_for_select([["US", "Canada"] , ["GB", "Germany"]], nil, divider: "----------") + grouped_options_for_select([["US", "Canada"], ["GB", "Germany"]], nil, divider: "----------") ) end @@ -386,7 +402,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_grouped_options_for_select_returns_html_safe_string - assert grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]]).html_safe? + assert_predicate grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]]), :html_safe? end def test_grouped_options_for_select_with_prompt_returns_html_escaped_string @@ -476,7 +492,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_time_zone_options_returns_html_safe_string - assert time_zone_options_for_select.html_safe? + assert_predicate time_zone_options_for_select, :html_safe? end def test_select diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 5e328ebf53..0d9bf77f98 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -142,14 +142,14 @@ class FormTagHelperTest < ActionView::TestCase actual = form_tag({}, { enforce_utf8: true }) expected = whole_form("http://www.example.com", enforce_utf8: true) assert_dom_equal expected, actual - assert actual.html_safe? + assert_predicate actual, :html_safe? end def test_form_tag_enforce_utf8_false actual = form_tag({}, { enforce_utf8: false }) expected = whole_form("http://www.example.com", enforce_utf8: false) assert_dom_equal expected, actual - assert actual.html_safe? + assert_predicate actual, :html_safe? end def test_form_tag_with_block_in_erb diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 402ee9b6ae..beee76f711 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -35,7 +35,7 @@ class LookupContextTest < ActiveSupport::TestCase test "allows me to freeze and retrieve frozen formats" do @lookup_context.formats.freeze - assert @lookup_context.formats.frozen? + assert_predicate @lookup_context.formats, :frozen? end test "provides getters and setters for variants" do diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index 2b671a6685..357ae1326a 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -79,7 +79,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal "1.23 <b>km3</b>", number_to_human(1_234_567_000_000, units: volume) assert_equal "1.23 <b>Pl</b>", number_to_human(1_234_567_000_000_000, units: volume) - #Including fractionals + # Including fractionals distance = { mili: "<b>mm</b>", centi: "<b>cm</b>", deci: "<b>dm</b>", unit: "<b>m</b>", ten: "<b>dam</b>", hundred: "<b>hm</b>", thousand: "<b>km</b>", micro: "<b>um</b>", nano: "<b>nm</b>", pico: "<b>pm</b>", femto: "<b>fm</b>" } @@ -126,43 +126,43 @@ class NumberHelperTest < ActionView::TestCase end def test_number_helpers_outputs_are_html_safe - assert number_to_human(1).html_safe? - assert !number_to_human("<script></script>").html_safe? - assert number_to_human("asdf".html_safe).html_safe? - assert number_to_human("1".html_safe).html_safe? - - assert number_to_human_size(1).html_safe? - assert number_to_human_size(1000000).html_safe? - assert !number_to_human_size("<script></script>").html_safe? - assert number_to_human_size("asdf".html_safe).html_safe? - assert number_to_human_size("1".html_safe).html_safe? - - assert number_with_precision(1, strip_insignificant_zeros: false).html_safe? - assert number_with_precision(1, strip_insignificant_zeros: true).html_safe? - assert !number_with_precision("<script></script>").html_safe? - assert number_with_precision("asdf".html_safe).html_safe? - assert number_with_precision("1".html_safe).html_safe? - - assert number_to_currency(1).html_safe? - assert !number_to_currency("<script></script>").html_safe? - assert number_to_currency("asdf".html_safe).html_safe? - assert number_to_currency("1".html_safe).html_safe? - - assert number_to_percentage(1).html_safe? - assert !number_to_percentage("<script></script>").html_safe? - assert number_to_percentage("asdf".html_safe).html_safe? - assert number_to_percentage("1".html_safe).html_safe? - - assert number_to_phone(1).html_safe? + assert_predicate number_to_human(1), :html_safe? + assert_not_predicate number_to_human("<script></script>"), :html_safe? + assert_predicate number_to_human("asdf".html_safe), :html_safe? + assert_predicate number_to_human("1".html_safe), :html_safe? + + assert_predicate number_to_human_size(1), :html_safe? + assert_predicate number_to_human_size(1000000), :html_safe? + assert_not_predicate number_to_human_size("<script></script>"), :html_safe? + assert_predicate number_to_human_size("asdf".html_safe), :html_safe? + assert_predicate number_to_human_size("1".html_safe), :html_safe? + + assert_predicate number_with_precision(1, strip_insignificant_zeros: false), :html_safe? + assert_predicate number_with_precision(1, strip_insignificant_zeros: true), :html_safe? + assert_not_predicate number_with_precision("<script></script>"), :html_safe? + assert_predicate number_with_precision("asdf".html_safe), :html_safe? + assert_predicate number_with_precision("1".html_safe), :html_safe? + + assert_predicate number_to_currency(1), :html_safe? + assert_not_predicate number_to_currency("<script></script>"), :html_safe? + assert_predicate number_to_currency("asdf".html_safe), :html_safe? + assert_predicate number_to_currency("1".html_safe), :html_safe? + + assert_predicate number_to_percentage(1), :html_safe? + assert_not_predicate number_to_percentage("<script></script>"), :html_safe? + assert_predicate number_to_percentage("asdf".html_safe), :html_safe? + assert_predicate number_to_percentage("1".html_safe), :html_safe? + + assert_predicate number_to_phone(1), :html_safe? assert_equal "<script></script>", number_to_phone("<script></script>") - assert number_to_phone("<script></script>").html_safe? - assert number_to_phone("asdf".html_safe).html_safe? - assert number_to_phone("1".html_safe).html_safe? - - assert number_with_delimiter(1).html_safe? - assert !number_with_delimiter("<script></script>").html_safe? - assert number_with_delimiter("asdf".html_safe).html_safe? - assert number_with_delimiter("1".html_safe).html_safe? + assert_predicate number_to_phone("<script></script>"), :html_safe? + assert_predicate number_to_phone("asdf".html_safe), :html_safe? + assert_predicate number_to_phone("1".html_safe), :html_safe? + + assert_predicate number_with_delimiter(1), :html_safe? + assert_not_predicate number_with_delimiter("<script></script>"), :html_safe? + assert_predicate number_with_delimiter("asdf".html_safe), :html_safe? + assert_predicate number_with_delimiter("1".html_safe), :html_safe? end def test_number_helpers_should_raise_error_if_invalid_when_specified diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb index b5e9a77105..faeeded1c8 100644 --- a/actionview/test/template/output_safety_helper_test.rb +++ b/actionview/test/template/output_safety_helper_test.rb @@ -12,7 +12,7 @@ class OutputSafetyHelperTest < ActionView::TestCase test "raw returns the safe string" do result = raw(@string) assert_equal @string, result - assert result.html_safe? + assert_predicate result, :html_safe? end test "raw handles nil values correctly" do @@ -53,11 +53,11 @@ class OutputSafetyHelperTest < ActionView::TestCase test "to_sentence should escape non-html_safe values" do actual = to_sentence(%w(< > & ' ")) - assert actual.html_safe? + assert_predicate actual, :html_safe? assert_equal("<, >, &, ', and "", actual) actual = to_sentence(%w(<script>)) - assert actual.html_safe? + assert_predicate actual, :html_safe? assert_equal("<script>", actual) end @@ -80,19 +80,19 @@ class OutputSafetyHelperTest < ActionView::TestCase url = "https://example.com" expected = %(<a href="#{url}">#{url}</a> and <p><marquee>shady stuff</marquee><br /></p>) actual = to_sentence([link_to(url, url), ptag]) - assert actual.html_safe? + assert_predicate actual, :html_safe? assert_equal(expected, actual) end test "to_sentence handles blank strings" do actual = to_sentence(["", "two", "three"]) - assert actual.html_safe? + assert_predicate actual, :html_safe? assert_equal ", two, and three", actual end test "to_sentence handles nil values" do actual = to_sentence([nil, "two", "three"]) - assert actual.html_safe? + assert_predicate actual, :html_safe? assert_equal ", two, and three", actual end diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index 0e690c82cb..181f09ab65 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -38,6 +38,6 @@ class SanitizeHelperTest < ActionView::TestCase end def test_sanitize_is_marked_safe - assert sanitize("<html><script></script></html>").html_safe? + assert_predicate sanitize("<html><script></script></html>"), :html_safe? end end diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb index 23edf7b538..ef000300cc 100644 --- a/actionview/test/template/streaming_render_test.rb +++ b/actionview/test/template/streaming_render_test.rb @@ -5,7 +5,7 @@ require "abstract_unit" class TestController < ActionController::Base end -class FiberedTest < ActiveSupport::TestCase +class SetupFiberedBase < ActiveSupport::TestCase def setup view_paths = ActionController::Base.view_paths @assigns = { secret: "in the sauce", name: nil } @@ -25,7 +25,9 @@ class FiberedTest < ActiveSupport::TestCase end string end +end +class FiberedTest < SetupFiberedBase def test_streaming_works content = [] body = render_body(template: "test/hello_world", layout: "layouts/yield") @@ -111,3 +113,20 @@ class FiberedTest < ActiveSupport::TestCase buffered_render(template: "test/streaming", layout: "layouts/streaming_with_capture") end end + +class FiberedWithLocaleTest < SetupFiberedBase + def setup + @old_locale = I18n.locale + I18n.locale = "da" + super + end + + def teardown + I18n.locale = @old_locale + end + + def test_render_with_streaming_and_locale + assert_equal "layout.locale: da\nview.locale: da\n\n", + buffered_render(template: "test/streaming_with_locale", layout: "layouts/streaming_with_locale") + end +end diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index 8c57803796..9a6226fd04 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -81,7 +81,7 @@ class TagHelperTest < ActionView::TestCase def test_content_tag assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create") - assert content_tag("a", "Create", "href" => "create").html_safe? + assert_predicate content_tag("a", "Create", "href" => "create"), :html_safe? assert_equal content_tag("a", "Create", "href" => "create"), content_tag("a", "Create", href: "create") assert_equal "<p><script>evil_js</script></p>", @@ -92,7 +92,7 @@ class TagHelperTest < ActionView::TestCase def test_tag_builder_with_content assert_equal "<div id=\"post_1\">Content</div>", tag.div("Content", id: "post_1") - assert tag.div("Content", id: "post_1").html_safe? + assert_predicate tag.div("Content", id: "post_1"), :html_safe? assert_equal tag.div("Content", id: "post_1"), tag.div("Content", "id": "post_1") assert_equal "<p><script>evil_js</script></p>", @@ -307,8 +307,8 @@ class TagHelperTest < ActionView::TestCase def test_tag_builder_disable_escaping assert_equal '<a href="&"></a>', tag.a(href: "&", escape_attributes: false) - assert_equal '<a href="&">cnt</a>', tag.a(href: "&" , escape_attributes: false) { "cnt" } - assert_equal '<br data-hidden="&">', tag.br("data-hidden": "&" , escape_attributes: false) + assert_equal '<a href="&">cnt</a>', tag.a(href: "&", escape_attributes: false) { "cnt" } + assert_equal '<br data-hidden="&">', tag.br("data-hidden": "&", escape_attributes: false) assert_equal '<a href="&">content</a>', tag.a("content", href: "&", escape_attributes: false) assert_equal '<a href="&">content</a>', tag.a(href: "&", escape_attributes: false) { "content" } end @@ -316,18 +316,18 @@ class TagHelperTest < ActionView::TestCase def test_data_attributes ["data", :data].each { |data| assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{"key":"value"}" data-string-with-quotes="double"quote"party"" data-string="hello" data-symbol="foo" />', - tag("a", data => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) + tag("a", data => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{"key":"value"}" data-string-with-quotes="double"quote"party"" data-string="hello" data-symbol="foo" />', - tag.a(data: { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) + tag.a(data: { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) } end def test_aria_attributes ["aria", :aria].each { |aria| assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{"key":"value"}" aria-string-with-quotes="double"quote"party"" aria-string="hello" aria-symbol="foo" />', - tag("a", aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) + tag("a", aria => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{"key":"value"}" aria-string-with-quotes="double"quote"party"" aria-string="hello" aria-symbol="foo" />', - tag.a(aria: { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) + tag.a(aria: { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) } end diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index f247de066f..45edfe18be 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -19,12 +19,12 @@ class TextHelperTest < ActionView::TestCase end def test_simple_format_should_be_html_safe - assert simple_format("<b> test with html tags </b>").html_safe? + assert_predicate simple_format("<b> test with html tags </b>"), :html_safe? end def test_simple_format_included_in_isolation helper_klass = Class.new { include ActionView::Helpers::TextHelper } - assert helper_klass.new.simple_format("<b> test with html tags </b>").html_safe? + assert_predicate helper_klass.new.simple_format("<b> test with html tags </b>"), :html_safe? end def test_simple_format @@ -123,7 +123,7 @@ class TextHelperTest < ActionView::TestCase end def test_truncate_should_be_html_safe - assert truncate("Hello World!", length: 12).html_safe? + assert_predicate truncate("Hello World!", length: 12), :html_safe? end def test_truncate_should_escape_the_input @@ -136,12 +136,12 @@ class TextHelperTest < ActionView::TestCase def test_truncate_with_escape_false_should_be_html_safe truncated = truncate("Hello <script>code!</script>World!!", length: 12, escape: false) - assert truncated.html_safe? + assert_predicate truncated, :html_safe? end def test_truncate_with_block_should_be_html_safe truncated = truncate("Here's a long test and I need a continue to read link", length: 27) { link_to "Continue", "#" } - assert truncated.html_safe? + assert_predicate truncated, :html_safe? end def test_truncate_with_block_should_escape_the_input @@ -156,7 +156,7 @@ class TextHelperTest < ActionView::TestCase def test_truncate_with_block_with_escape_false_should_be_html_safe truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", length: 27, escape: false) { link_to "Continue", "#" } - assert truncated.html_safe? + assert_predicate truncated, :html_safe? end def test_truncate_with_block_should_escape_the_block @@ -165,7 +165,7 @@ class TextHelperTest < ActionView::TestCase end def test_highlight_should_be_html_safe - assert highlight("This is a beautiful morning", "beautiful").html_safe? + assert_predicate highlight("This is a beautiful morning", "beautiful"), :html_safe? end def test_highlight @@ -297,7 +297,7 @@ class TextHelperTest < ActionView::TestCase end def test_excerpt_should_not_be_html_safe - assert !excerpt("This is a beautiful! morning", "beautiful", radius: 5).html_safe? + assert_not_predicate excerpt("This is a beautiful! morning", "beautiful", radius: 5), :html_safe? end def test_excerpt_in_borderline_cases diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index 8956a584ff..f40595bf4d 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -75,7 +75,7 @@ class TranslationHelperTest < ActiveSupport::TestCase def test_returns_missing_translation_message_with_unescaped_interpolation expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &quot; onclick=&quot;alert()&quot;">Missing</span>' assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"}) - assert translate(:"translations.missing").html_safe? + assert_predicate translate(:"translations.missing"), :html_safe? end def test_returns_missing_translation_message_does_filters_out_i18n_options @@ -145,11 +145,11 @@ class TranslationHelperTest < ActiveSupport::TestCase end def test_translate_marks_translations_named_html_as_safe_html - assert translate(:'translations.html').html_safe? + assert_predicate translate(:'translations.html'), :html_safe? end def test_translate_marks_translations_with_a_html_suffix_as_safe_html - assert translate(:'translations.hello_html').html_safe? + assert_predicate translate(:'translations.hello_html'), :html_safe? end def test_translate_escapes_interpolations_in_translations_with_a_html_suffix diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 0cd0386cac..8bccda481b 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -663,7 +663,7 @@ class UrlHelperTest < ActiveSupport::TestCase end def test_mail_to_returns_html_safe_string - assert mail_to("david@loudthinking.com").html_safe? + assert_predicate mail_to("david@loudthinking.com"), :html_safe? end def test_mail_to_with_block diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 0c69a5c663..ec99b5d975 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,5 +1,3 @@ -* Support redis-rb 4.0. - *Jeremy Daer* -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE index daa726b9f0..274211f710 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2017 David Heinemeier Hansson +Copyright (c) 2014-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activejob/README.md b/activejob/README.md index 8a9a23929b..f1ebb76e08 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -100,7 +100,7 @@ The latest version of Active Job can be installed with RubyGems: $ gem install activejob ``` -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/activejob @@ -117,7 +117,7 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/activejob/Rakefile b/activejob/Rakefile index 6f13ef449d..77f3e7f8ff 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -2,7 +2,7 @@ require "rake/testtask" -#TODO: add qu back to the list after it support Rails 5.1 +# TODO: add qu back to the list after it support Rails 5.1 ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION) diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 1f5bd7b3e9..626abaa767 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2014-2017 David Heinemeier Hansson +# Copyright (c) 2014-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index c4e12fc518..879746fc01 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -97,17 +97,23 @@ module ActiveJob # ==== Examples # # class DeliverWebhookJob < ActiveJob::Base + # attr_writer :attempt_number + # + # def attempt_number + # @attempt_number ||= 0 + # end + # # def serialize - # super.merge('attempt_number' => (@attempt_number || 0) + 1) + # super.merge('attempt_number' => attempt_number + 1) # end # # def deserialize(job_data) # super - # @attempt_number = job_data['attempt_number'] + # self.attempt_number = job_data['attempt_number'] # end # - # rescue_from(TimeoutError) do |exception| - # raise exception if @attempt_number > 5 + # rescue_from(Timeout::Error) do |exception| + # raise exception if attempt_number > 5 # retry_job(wait: 10) # end # end diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index 8b4a88ba6a..ae700848d0 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -61,18 +61,28 @@ module ActiveJob # Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job, # like an Active Record, is no longer available, and the job is thus no longer relevant. # + # You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter. + # # ==== Example # # class SearchIndexingJob < ActiveJob::Base # discard_on ActiveJob::DeserializationError + # discard_on(CustomAppException) do |job, exception| + # ExceptionNotifier.caught(exception) + # end # # def perform(record) # # Will raise ActiveJob::DeserializationError if the record can't be deserialized + # # Might raise CustomAppException for something domain specific # end # end def discard_on(exception) rescue_from exception do |error| - logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}." + if block_given? + yield self, exception + else + logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}." + end end end end diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 7ee61780e1..770f70dc5e 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -7,8 +7,8 @@ module ActiveJob end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index dd05800baf..006a683b85 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -29,28 +29,22 @@ module ActiveJob # Specify the backend queue provider. The default queue adapter # is the +:async+ queue. See QueueAdapters for more # information. - def queue_adapter=(name_or_adapter_or_class) - interpret_adapter(name_or_adapter_or_class) - end - - private - - def interpret_adapter(name_or_adapter_or_class) - case name_or_adapter_or_class - when Symbol, String - assign_adapter(name_or_adapter_or_class.to_s, - ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new) + def queue_adapter=(name_or_adapter) + case name_or_adapter + when Symbol, String + queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new + assign_adapter(name_or_adapter.to_s, queue_adapter) + else + if queue_adapter?(name_or_adapter) + adapter_name = "#{name_or_adapter.class.name.demodulize.remove('Adapter').underscore}" + assign_adapter(adapter_name, name_or_adapter) else - if queue_adapter?(name_or_adapter_or_class) - adapter_name = "#{name_or_adapter_or_class.class.name.demodulize.remove('Adapter').underscore}" - assign_adapter(adapter_name, - name_or_adapter_or_class) - else - raise ArgumentError - end + raise ArgumentError end end + end + private def assign_adapter(adapter_name, queue_adapter) self._queue_adapter_name = adapter_name self._queue_adapter = queue_adapter diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb index 1978179948..8eeef32b99 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -34,6 +34,10 @@ module ActiveJob @job_data = job_data end + def display_name + "#{job_data['job_class']} [#{job_data['job_id']}] from DelayedJob(#{job_data['queue_name']}) with arguments: #{job_data['arguments']}" + end + def perform Base.execute(job_data) end diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb index 5a1135854b..f726e6ad93 100644 --- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -18,7 +18,7 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :sidekiq class SidekiqAdapter def enqueue(job) #:nodoc: - #Sidekiq::Client does not support symbols as keys + # Sidekiq::Client does not support symbols as keys job.provider_job_id = Sidekiq::Client.push \ "class" => JobWrapper, "wrapped" => job.class.to_s, diff --git a/activejob/lib/rails/generators/job/templates/application_job.rb b/activejob/lib/rails/generators/job/templates/application_job.rb.tt index f93745a31a..f93745a31a 100644 --- a/activejob/lib/rails/generators/job/templates/application_job.rb +++ b/activejob/lib/rails/generators/job/templates/application_job.rb.tt diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb.tt index 4ad2914a45..4ad2914a45 100644 --- a/activejob/lib/rails/generators/job/templates/job.rb +++ b/activejob/lib/rails/generators/job/templates/job.rb.tt diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 13e6fcb727..7e7f854da0 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -12,7 +12,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, - "a", true, false, BigDecimal.new(5), + "a", true, false, BigDecimal(5), [ 1, "a" ], { "a" => 1 } ].each do |arg| diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb index 22fed0a808..bc33d79f61 100644 --- a/activejob/test/cases/exceptions_test.rb +++ b/activejob/test/cases/exceptions_test.rb @@ -58,6 +58,13 @@ class ExceptionsTest < ActiveJob::TestCase end end + test "custom handling of discarded job" do + perform_enqueued_jobs do + RetryJob.perform_later "CustomDiscardableError", 2 + assert_equal "Dealt with a job that was discarded in a custom way", JobBuffer.last_value + end + end + test "custom handling of job that exceeds retry attempts" do perform_enqueued_jobs do RetryJob.perform_later "CustomCatchError", 6 diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 0d8aa336a6..32ef485c45 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -45,6 +45,13 @@ class QueuingTest < ActiveSupport::TestCase end end + test "should supply a wrapped class name to DelayedJob" do + skip unless adapter_is?(:delayed_job) + ::HelloJob.perform_later + job = Delayed::Job.first + assert_match(/HelloJob \[[0-9a-f-]+\] from DelayedJob\(default\) with arguments: \[\]/, job.name) + end + test "resque JobWrapper should have instance variable queue" do skip unless adapter_is?(:resque) job = ::HelloJob.set(wait: 5.seconds).perform_later diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb index 9aa99d9a21..82b80fe12b 100644 --- a/activejob/test/jobs/retry_job.rb +++ b/activejob/test/jobs/retry_job.rb @@ -10,6 +10,7 @@ class ExponentialWaitTenAttemptsError < StandardError; end class CustomWaitTenAttemptsError < StandardError; end class CustomCatchError < StandardError; end class DiscardableError < StandardError; end +class CustomDiscardableError < StandardError; end class RetryJob < ActiveJob::Base retry_on DefaultsError @@ -19,6 +20,7 @@ class RetryJob < ActiveJob::Base retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10 retry_on(CustomCatchError) { |job, exception| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{exception.message}") } discard_on DiscardableError + discard_on(CustomDiscardableError) { |job, exception| JobBuffer.add("Dealt with a job that was discarded in a custom way") } def perform(raising, attempts) if executions < attempts diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb index 9280a37a5c..1691896b7c 100644 --- a/activejob/test/support/delayed_job/delayed/backend/test.rb +++ b/activejob/test/support/delayed_job/delayed/backend/test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb +# copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb require "ostruct" # An in-memory backend suitable only for testing. Tries to behave as if it were an ORM. diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb index 7d5174b957..2ed8302277 100644 --- a/activejob/test/support/integration/adapters/resque.rb +++ b/activejob/test/support/integration/adapters/resque.rb @@ -3,11 +3,12 @@ module ResqueJobsManager def setup ActiveJob::Base.queue_adapter = :resque - Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.new(url: "redis://:password@127.0.0.1:6379/12", thread_safe: true) + Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.new(url: "redis://127.0.0.1:6379/12", thread_safe: true) Resque.logger = Rails.logger unless can_run? puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n" - exit + status = ENV["CI"] ? false : true + exit status end end diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index ceb7fb61f2..c79de12eaf 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -5,20 +5,13 @@ require "sidekiq/api" require "sidekiq/testing" Sidekiq::Testing.disable! -Sidekiq.configure_server do |config| - config.redis = { url: "redis://:password@127.0.0.1:6379/12" } -end - -Sidekiq.configure_client do |config| - config.redis = { url: "redis://:password@127.0.0.1:6379/12" } -end - module SidekiqJobsManager def setup ActiveJob::Base.queue_adapter = :sidekiq unless can_run? puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n" - exit + status = ENV["CI"] ? false : true + exit status end end @@ -58,6 +51,7 @@ module SidekiqJobsManager self_write.puts("TERM") end + require "sidekiq/cli" require "sidekiq/launcher" sidekiq = Sidekiq::Launcher.new(queues: ["integration_tests"], environment: "test", diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 794744c646..13ea7d0bf3 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,49 +1,3 @@ -* Execute `ConfirmationValidator` validation when `_confirmation`'s value is `false`. - *bogdanvlviv* -* Allow passing a Proc or Symbol to length validator options. - - *Matt Rohrer* - -* Add method `#merge!` for `ActiveModel::Errors`. - - *Jahfer Husain* - -* Fix regression in numericality validator when comparing Decimal and Float input - values with more scale than the schema. - - *Bradley Priest* - -* Fix methods `#keys`, `#values` in `ActiveModel::Errors`. - - Change `#keys` to only return the keys that don't have empty messages. - - Change `#values` to only return the not empty values. - - Example: - - # Before - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [:name] - person.errors.values # => [[]] - - # After - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [] - person.errors.values # => [] - - *bogdanvlviv* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes. diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index ac810e86d0..1cb3add0fc 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 772df0f8f6..1aaf4813ea 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -251,11 +251,11 @@ Active Model is released under the MIT license: == Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index a3884206a6..bc10d6b4b9 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index b75ff80b31..27f3e38e31 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -233,6 +233,10 @@ module ActiveModel false end + def forgetting_assignment + dup + end + def with_type(type) self.class.new(name, type) end diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb index f274b687d4..a5dc6188d2 100644 --- a/activemodel/lib/active_model/attribute/user_provided_default.rb +++ b/activemodel/lib/active_model/attribute/user_provided_default.rb @@ -22,6 +22,28 @@ module ActiveModel self.class.new(name, user_provided_value, type, original_attribute) end + def marshal_dump + result = [ + name, + value_before_type_cast, + type, + original_attribute, + ] + result << value if defined?(@value) + result + end + + def marshal_load(values) + name, user_provided_value, type, original_attribute, value = values + @name = name + @user_provided_value = user_provided_value + @type = type + @original_attribute = original_attribute + if values.length == 5 + @value = value + end + end + protected attr_reader :user_provided_value diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index c67e1b809a..f55613ecd5 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -35,6 +35,10 @@ module ActiveModel end end + def changed_attribute_names + attr_names.select { |attr| changed?(attr) } + end + def any_changes? attr_names.any? { |attr| changed?(attr) } end @@ -109,8 +113,5 @@ module ActiveModel def original_value(*) end - - def force_change(*) - end end end diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb index a892accbc6..a890ee3932 100644 --- a/activemodel/lib/active_model/attribute_set.rb +++ b/activemodel/lib/active_model/attribute_set.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true +require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set/builder" require "active_model/attribute_set/yaml_encoder" module ActiveModel class AttributeSet # :nodoc: - delegate :each_value, :fetch, to: :attributes + delegate :each_value, :fetch, :except, to: :attributes def initialize(attributes) @attributes = attributes diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb index f94f47370f..bf2d06b48a 100644 --- a/activemodel/lib/active_model/attribute_set/builder.rb +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -5,35 +5,30 @@ require "active_model/attribute" module ActiveModel class AttributeSet # :nodoc: class Builder # :nodoc: - attr_reader :types, :always_initialized, :default + attr_reader :types, :default_attributes - def initialize(types, always_initialized = nil, &default) + def initialize(types, default_attributes = {}) @types = types - @always_initialized = always_initialized - @default = default + @default_attributes = default_attributes end def build_from_database(values = {}, additional_types = {}) - if always_initialized && !values.key?(always_initialized) - values[always_initialized] = nil - end - - attributes = LazyAttributeHash.new(types, values, additional_types, &default) + attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes) AttributeSet.new(attributes) end end end class LazyAttributeHash # :nodoc: - delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize + delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize - def initialize(types, values, additional_types, &default) + def initialize(types, values, additional_types, default_attributes, delegate_hash = {}) @types = types @values = values @additional_types = additional_types @materialized = false - @delegate_hash = {} - @default = default || proc {} + @delegate_hash = delegate_hash + @default_attributes = default_attributes end def key?(key) @@ -81,20 +76,22 @@ module ActiveModel end def marshal_dump - materialize + [@types, @values, @additional_types, @default_attributes, @delegate_hash] end - def marshal_load(delegate_hash) - @delegate_hash = delegate_hash - @types = {} - @values = {} - @additional_types = {} - @materialized = true + def marshal_load(values) + if values.is_a?(Hash) + empty_hash = {}.freeze + initialize(empty_hash, empty_hash, empty_hash, empty_hash, values) + @materialized = true + else + initialize(*values) + end end protected - attr_reader :types, :values, :additional_types, :delegate_hash, :default + attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes def materialize unless @materialized @@ -117,7 +114,12 @@ module ActiveModel if value_present delegate_hash[name] = Attribute.from_database(name, value, type) elsif types.key?(name) - delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type) + attr = default_attributes[name] + if attr + delegate_hash[name] = attr.dup + else + delegate_hash[name] = Attribute.uninitialized(name, type) + end end end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index cac461b549..046ae67ad7 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set" require "active_model/attribute/user_provided_default" @@ -24,7 +23,7 @@ module ActiveModel end self.attribute_types = attribute_types.merge(name => type) define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type) - define_attribute_methods(name) + define_attribute_method(name) end private diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index d2ebd18107..0044fde6c5 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -3,6 +3,7 @@ require "active_support/hash_with_indifferent_access" require "active_support/core_ext/object/duplicable" require "active_model/attribute_mutation_tracker" +require "active_model/attribute_set" module ActiveModel # == Active \Model \Dirty @@ -142,9 +143,8 @@ module ActiveModel end def changes_applied # :nodoc: - @previously_changed = changes + _prepare_changes @mutations_before_last_save = mutations_from_database - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end @@ -155,7 +155,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.present? + mutations_from_database.any_changes? end # Returns an array with the name of the attributes with unsaved changes. @@ -164,24 +164,24 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - changed_attributes.keys + mutations_from_database.changed_attribute_names end # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: - !!changes_include?(attr) && + !!mutations_from_database.changed?(attr) && (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && - (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) + (from == OPTION_NOT_GIVEN || from == attribute_was(attr)) end # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) + mutations_from_database.original_value(attr) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. def attribute_previously_changed?(attr) #:nodoc: - previous_changes_include?(attr) + mutations_before_last_save.changed?(attr) end # Restore all previous data of the provided attributes. @@ -191,15 +191,12 @@ module ActiveModel # Clears all dirty data: current changes and previous changes. def clear_changes_information - @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end def clear_attribute_changes(attr_names) - attributes_changed_by_setter.except!(*attr_names) attr_names.each do |attr_name| clear_attribute_change(attr_name) end @@ -212,13 +209,7 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - # This should only be set by methods which will call changed_attributes - # multiple times when it is known that the computed value cannot change. - if defined?(@cached_changed_attributes) - @cached_changed_attributes - else - attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze - end + mutations_from_database.changed_values.freeze end # Returns a hash of changed attributes indicating their original @@ -228,9 +219,8 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - cache_changed_attributes do - ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] - end + _prepare_changes + mutations_from_database.changes end # Returns a hash of attributes that were changed before the model was saved. @@ -240,8 +230,7 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new - @previously_changed.merge(mutations_before_last_save.changes) + mutations_before_last_save.changes end def attribute_changed_in_place?(attr_name) # :nodoc: @@ -257,11 +246,17 @@ module ActiveModel unless defined?(@mutations_from_database) @mutations_from_database = nil end - @mutations_from_database ||= if defined?(@attributes) - ActiveModel::AttributeMutationTracker.new(@attributes) - else - NullMutationTracker.instance + + unless defined?(@attributes) + @_pseudo_attributes = true + @attributes = AttributeSet.new( + Hash.new { |h, attr| + h[attr] = Attribute.with_cast_value(attr, _clone_attribute(attr), Type.default_value) + } + ) end + + @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes) end def forget_attribute_assignments @@ -272,68 +267,45 @@ module ActiveModel @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance end - def cache_changed_attributes - @cached_changed_attributes = changed_attributes - yield - ensure - clear_changed_attributes_cache - end - - def clear_changed_attributes_cache - remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) - end - - # Returns +true+ if attr_name is changed, +false+ otherwise. - def changes_include?(attr_name) - attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name) - end - alias attribute_changed_by_setter? changes_include? - - # Returns +true+ if attr_name were changed before the model was saved, - # +false+ otherwise. - def previous_changes_include?(attr_name) - previous_changes.include?(attr_name) - end - # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) - [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) + [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + mutations_before_last_save.change_to_attribute(attr) end # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) - unless attribute_changed?(attr) - begin - value = _read_attribute(attr) - value = value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - end - - set_attribute_was(attr, value) + attr = attr.to_s + mutations_from_database.force_change(attr).tap do + @attributes[attr] if defined?(@_pseudo_attributes) end - mutations_from_database.force_change(attr) end # Handles <tt>restore_*!</tt> for +method_missing+. def restore_attribute!(attr) if attribute_changed?(attr) - __send__("#{attr}=", changed_attributes[attr]) + __send__("#{attr}=", attribute_was(attr)) clear_attribute_changes([attr]) end end - def attributes_changed_by_setter - @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new + def _prepare_changes + if defined?(@_pseudo_attributes) + changed.each do |attr| + @attributes.write_from_user(attr, _read_attribute(attr)) + end + end end - # Force an attribute to have a particular "before" value - def set_attribute_was(attr, old_value) - attributes_changed_by_setter[attr] = old_value + def _clone_attribute(attr) + value = _read_attribute(attr) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value end end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 971bdd08b1..275e3f1313 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -322,9 +322,13 @@ module ActiveModel # person.errors.added? :name, :too_long # => false # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) - message = message.call if message.respond_to?(:call) - message = normalize_message(attribute, message, options) - self[attribute].include? message + if message.is_a? Symbol + self.details[attribute].map { |e| e[:error] }.include? message + else + message = message.call if message.respond_to?(:call) + message = normalize_message(attribute, message, options) + self[attribute].include? message + end end # Returns all the full error messages in an array. diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 39269c159c..cef5441e4a 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -7,8 +7,8 @@ module ActiveModel end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 34d9ac6c96..b7ceabb59a 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -29,7 +29,7 @@ module ActiveModel # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes # of the model, and is used to a generate unique DOM id for the object. def test_to_key - assert model.respond_to?(:to_key), "The model should respond to to_key" + assert_respond_to model, :to_key def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end @@ -44,7 +44,7 @@ module ActiveModel # tests for this behavior in lint because it doesn't make sense to force # any of the possible implementation strategies on the implementer. def test_to_param - assert model.respond_to?(:to_param), "The model should respond to to_param" + assert_respond_to model, :to_param def model.to_key() [1] end def model.persisted?() false end assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" @@ -56,7 +56,7 @@ module ActiveModel # <tt>to_partial_path</tt> is used for looking up partials. For example, # a BlogPost model might return "blog_posts/blog_post". def test_to_partial_path - assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" + assert_respond_to model, :to_partial_path assert_kind_of String, model.to_partial_path end @@ -68,7 +68,7 @@ module ActiveModel # will route to the create action. If it is persisted, a form for the # object will route to the update action. def test_persisted? - assert model.respond_to?(:persisted?), "The model should respond to persisted?" + assert_respond_to model, :persisted? assert_boolean model.persisted?, "persisted?" end @@ -79,14 +79,14 @@ module ActiveModel # # Check ActiveModel::Naming for more information. def test_model_naming - assert model.class.respond_to?(:model_name), "The model class should respond to model_name" + assert_respond_to model.class, :model_name model_name = model.class.model_name - assert model_name.respond_to?(:to_str) - assert model_name.human.respond_to?(:to_str) - assert model_name.singular.respond_to?(:to_str) - assert model_name.plural.respond_to?(:to_str) + assert_respond_to model_name, :to_str + assert_respond_to model_name.human, :to_str + assert_respond_to model_name.singular, :to_str + assert_respond_to model_name.plural, :to_str - assert model.respond_to?(:model_name), "The model instance should respond to model_name" + assert_respond_to model, :model_name assert_equal model.model_name, model.class.model_name end @@ -100,13 +100,13 @@ module ActiveModel # If localization is used, the strings should be localized for the current # locale. If no error is present, the method should return an empty array. def test_errors_aref - assert model.respond_to?(:errors), "The model should respond to errors" + assert_respond_to model, :errors assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" end private def model - assert @model.respond_to?(:to_model), "The object should respond to to_model" + assert_respond_to @model, :to_model @model.to_model end diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index 39324999c9..1d7a26fff5 100644 --- a/activemodel/lib/active_model/type.rb +++ b/activemodel/lib/active_model/type.rb @@ -24,7 +24,7 @@ module ActiveModel class << self attr_accessor :registry # :nodoc: - # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup + # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup def register(type_name, klass = nil, **options, &block) registry.register(type_name, klass, **options, &block) end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index cdf11d190f..7f14d102dd 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -164,14 +164,14 @@ module ActiveModel if options.key?(:on) options = options.dup + options[:on] = Array(options[:on]) options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - !(Array(options[:on]) & Array(o.validation_context)).empty? + !(options[:on] & Array(o.validation_context)).empty? } end - args << options - set_callback(:validate, *args, &block) + set_callback(:validate, *args, options, &block) end # List all validators that are being used to validate the model using diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4d0ab2a2fe..887d31ae2a 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -54,15 +54,18 @@ module ActiveModel # person.valid? # => true # person.name # => "bob" def before_validation(*args, &block) - options = args.last - if options.is_a?(Hash) && options[:on] - options[:if] = Array(options[:if]) + options = args.extract_options! + + if options.key?(:on) + options = options.dup options[:on] = Array(options[:on]) + options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - options[:on].include? o.validation_context + !(options[:on] & Array(o.validation_context)).empty? } end - set_callback(:validation, :before, *args, &block) + + set_callback(:validation, :before, *args, options, &block) end # Defines a callback that will get called right after validation. @@ -93,15 +96,18 @@ module ActiveModel # person.status # => true def after_validation(*args, &block) options = args.extract_options! + options = options.dup options[:prepend] = true - options[:if] = Array(options[:if]) - if options[:on] + + if options.key?(:on) options[:on] = Array(options[:on]) + options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - options[:on].include? o.validation_context + !(options[:on] & Array(o.validation_context)).empty? } end - set_callback(:validation, :after, *(args << options), &block) + + set_callback(:validation, :after, *args, options, &block) end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 43d9f82d9f..e28e7e9219 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -154,7 +154,7 @@ module ActiveModel # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. def _validates_default_keys - [:if, :unless, :on, :allow_blank, :allow_nil , :strict] + [:if, :unless, :on, :allow_blank, :allow_nil, :strict] end def _parse_validates_options(options) diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index d2837ec894..0cfc6f4b6b 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -220,7 +220,7 @@ class AttributeMethodsTest < ActiveModel::TestCase ModelWithAttributes.define_attribute_methods(:foo) ModelWithAttributes.undefine_attribute_methods - assert !ModelWithAttributes.new.respond_to?(:foo) + assert_not_respond_to ModelWithAttributes.new, :foo assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end @@ -255,7 +255,7 @@ class AttributeMethodsTest < ActiveModel::TestCase m = ModelWithAttributes2.new m.attributes = { "private_method" => "<3", "protected_method" => "O_o" } - assert !m.respond_to?(:private_method) + assert_not_respond_to m, :private_method assert m.respond_to?(:private_method, true) c = ClassWithProtected.new diff --git a/activemodel/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb index 02c44c5d45..b868dba743 100644 --- a/activemodel/test/cases/attribute_set_test.rb +++ b/activemodel/test/cases/attribute_set_test.rb @@ -74,8 +74,8 @@ module ActiveModel clone.freeze - assert clone.frozen? - assert_not attributes.frozen? + assert_predicate clone, :frozen? + assert_not_predicate attributes, :frozen? end test "to_hash returns a hash of the type cast values" do @@ -105,8 +105,8 @@ module ActiveModel test "known columns are built with uninitialized attributes" do attributes = attributes_with_uninitialized_key - assert attributes[:foo].initialized? - assert_not attributes[:bar].initialized? + assert_predicate attributes[:foo], :initialized? + assert_not_predicate attributes[:bar], :initialized? end test "uninitialized attributes are not included in the attributes hash" do @@ -163,12 +163,13 @@ module ActiveModel end test "the primary_key is always initialized" do - builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo) + defaults = { foo: Attribute.from_user(:foo, nil, nil) } + builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults) attributes = builder.build_from_database assert attributes.key?(:foo) assert_equal [:foo], attributes.keys - assert attributes[:foo].initialized? + assert_predicate attributes[:foo], :initialized? end class MyType @@ -208,11 +209,6 @@ module ActiveModel assert_equal "value from user", attributes.fetch_value(:foo) end - def attributes_with_uninitialized_key - builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) - builder.build_from_database(foo: "1.1") - end - test "freezing doesn't prevent the set from materializing" do builder = AttributeSet::Builder.new(foo: Type::String.new) attributes = builder.build_from_database(foo: "1") @@ -221,6 +217,22 @@ module ActiveModel assert_equal({ foo: "1" }, attributes.to_hash) end + test "marshaling dump/load legacy materialized attribute hash" do + builder = AttributeSet::Builder.new(foo: Type::String.new) + attributes = builder.build_from_database(foo: "1") + + attributes.instance_variable_get(:@attributes).instance_eval do + class << self + def marshal_dump + materialize + end + end + end + + attributes = Marshal.load(Marshal.dump(attributes)) + assert_equal({ foo: "1" }, attributes.to_hash) + end + test "#accessed_attributes returns only attributes which have been read" do builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new) attributes = builder.build_from_database(foo: "1", bar: "2") @@ -252,5 +264,11 @@ module ActiveModel assert_equal attributes, attributes2 assert_not_equal attributes2, attributes3 end + + private + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: "1.1") + end end end diff --git a/activemodel/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb index 14d86cef97..ea2b0efd11 100644 --- a/activemodel/test/cases/attribute_test.rb +++ b/activemodel/test/cases/attribute_test.rb @@ -175,33 +175,33 @@ module ActiveModel test "an attribute has not been read by default" do attribute = Attribute.from_database(:foo, 1, Type::Value.new) - assert_not attribute.has_been_read? + assert_not_predicate attribute, :has_been_read? end test "an attribute has been read when its value is calculated" do attribute = Attribute.from_database(:foo, 1, Type::Value.new) attribute.value - assert attribute.has_been_read? + assert_predicate attribute, :has_been_read? end test "an attribute is not changed if it hasn't been assigned or mutated" do attribute = Attribute.from_database(:foo, 1, Type::Value.new) - refute attribute.changed? + assert_not_predicate attribute, :changed? end test "an attribute is changed if it's been assigned a new value" do attribute = Attribute.from_database(:foo, 1, Type::Value.new) changed = attribute.with_value_from_user(2) - assert changed.changed? + assert_predicate changed, :changed? end test "an attribute is not changed if it's assigned the same value" do attribute = Attribute.from_database(:foo, 1, Type::Value.new) unchanged = attribute.with_value_from_user(1) - refute unchanged.changed? + assert_not_predicate unchanged, :changed? end test "an attribute can not be mutated if it has not been read, @@ -209,15 +209,15 @@ module ActiveModel type_which_raises_from_all_methods = Object.new attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods) - assert_not attribute.changed_in_place? + assert_not_predicate attribute, :changed_in_place? end test "an attribute is changed if it has been mutated" do attribute = Attribute.from_database(:foo, "bar", Type::String.new) attribute.value << "!" - assert attribute.changed_in_place? - assert attribute.changed? + assert_predicate attribute, :changed_in_place? + assert_predicate attribute, :changed? end test "an attribute can forget its changes" do @@ -226,7 +226,7 @@ module ActiveModel forgotten = changed.forgetting_assignment assert changed.changed? # sanity check - refute forgotten.changed? + assert_not_predicate forgotten, :changed? end test "with_value_from_user validates the value" do diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb index 83a86371e0..c991176389 100644 --- a/activemodel/test/cases/attributes_dirty_test.rb +++ b/activemodel/test/cases/attributes_dirty_test.rb @@ -25,11 +25,11 @@ class AttributesDirtyTest < ActiveModel::TestCase end test "setting attribute will result in change" do - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? @model.name = "Ringo" - assert @model.changed? - assert @model.name_changed? + assert_predicate @model, :changed? + assert_predicate @model, :name_changed? end test "list of changed attribute keys" do @@ -71,35 +71,35 @@ class AttributesDirtyTest < ActiveModel::TestCase test "attribute mutation" do @model.name = "Yam" @model.save - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? @model.name.replace("Hadad") - assert @model.name_changed? + assert_predicate @model, :name_changed? end test "resetting attribute" do @model.name = "Bob" @model.restore_name! assert_nil @model.name - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? end test "setting color to same value should not result in change being recorded" do @model.color = "red" - assert @model.color_changed? + assert_predicate @model, :color_changed? @model.save - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? @model.color = "red" - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? end test "saving should reset model's changed status" do @model.name = "Alf" - assert @model.changed? + assert_predicate @model, :changed? @model.save - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? end test "saving should preserve previous changes" do @@ -118,7 +118,7 @@ class AttributesDirtyTest < ActiveModel::TestCase test "saving should preserve model's previous changed status" do @model.name = "Jericho Cane" @model.save - assert @model.name_previously_changed? + assert_predicate @model, :name_previously_changed? end test "previous value is preserved when changed after save" do @@ -143,7 +143,7 @@ class AttributesDirtyTest < ActiveModel::TestCase test "using attribute_will_change! with a symbol" do @model.size = 1 - assert @model.size_changed? + assert_predicate @model, :size_changed? end test "reload should reset all changes" do @@ -170,7 +170,7 @@ class AttributesDirtyTest < ActiveModel::TestCase @model.restore_attributes - assert_not @model.changed? + assert_not_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "Red", @model.color end @@ -184,7 +184,7 @@ class AttributesDirtyTest < ActiveModel::TestCase @model.restore_attributes(["name"]) - assert @model.changed? + assert_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "White", @model.color end diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb index 914aee1ac0..7c1d813ce0 100644 --- a/activemodel/test/cases/attributes_test.rb +++ b/activemodel/test/cases/attributes_test.rb @@ -33,7 +33,7 @@ module ActiveModel assert_equal 2, data.integer_field assert_equal "Rails FTW", data.string_field - assert_equal BigDecimal.new("12.3"), data.decimal_field + assert_equal BigDecimal("12.3"), data.decimal_field assert_equal "default string", data.string_with_default assert_equal Date.new(2016, 1, 1), data.date_field assert_equal false, data.boolean_field @@ -64,5 +64,14 @@ module ActiveModel assert_equal "4.4", data.integer_field end + + test "attributes with proc defaults can be marshalled" do + data = ModelForAttributesTest.new + attributes = data.instance_variable_get(:@attributes) + round_tripped = Marshal.load(Marshal.dump(data)) + new_attributes = round_tripped.instance_variable_get(:@attributes) + + assert_equal attributes, new_attributes + end end end diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index a5d29d0f22..1ec12d8222 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -85,21 +85,21 @@ class CallbacksTest < ActiveModel::TestCase end test "only selects which types of callbacks should be created" do - assert !ModelCallbacks.respond_to?(:before_initialize) - assert !ModelCallbacks.respond_to?(:around_initialize) + assert_not_respond_to ModelCallbacks, :before_initialize + assert_not_respond_to ModelCallbacks, :around_initialize assert_respond_to ModelCallbacks, :after_initialize end test "only selects which types of callbacks should be created from an array list" do assert_respond_to ModelCallbacks, :before_multiple assert_respond_to ModelCallbacks, :around_multiple - assert !ModelCallbacks.respond_to?(:after_multiple) + assert_not_respond_to ModelCallbacks, :after_multiple end test "no callbacks should be created" do - assert !ModelCallbacks.respond_to?(:before_empty) - assert !ModelCallbacks.respond_to?(:around_empty) - assert !ModelCallbacks.respond_to?(:after_empty) + assert_not_respond_to ModelCallbacks, :before_empty + assert_not_respond_to ModelCallbacks, :around_empty + assert_not_respond_to ModelCallbacks, :after_empty end class Violin diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index dfe041ff50..f769eb0da1 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -5,12 +5,13 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods :name, :color, :size + define_attribute_methods :name, :color, :size, :status def initialize @name = nil @color = nil @size = nil + @status = "initialized" end def name @@ -40,6 +41,15 @@ class DirtyTest < ActiveModel::TestCase @size = val end + def status + @status + end + + def status=(val) + status_will_change! unless val == @status + @status = val + end + def save changes_applied end @@ -54,11 +64,11 @@ class DirtyTest < ActiveModel::TestCase end test "setting attribute will result in change" do - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? @model.name = "Ringo" - assert @model.changed? - assert @model.name_changed? + assert_predicate @model, :changed? + assert_predicate @model, :name_changed? end test "list of changed attribute keys" do @@ -99,82 +109,93 @@ class DirtyTest < ActiveModel::TestCase test "attribute mutation" do @model.instance_variable_set("@name", "Yam".dup) - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? @model.name.replace("Hadad") - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? @model.name_will_change! @model.name.replace("Baal") - assert @model.name_changed? + assert_predicate @model, :name_changed? end test "resetting attribute" do @model.name = "Bob" @model.restore_name! assert_nil @model.name - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? end test "setting color to same value should not result in change being recorded" do @model.color = "red" - assert @model.color_changed? + assert_predicate @model, :color_changed? @model.save - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? @model.color = "red" - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? end test "saving should reset model's changed status" do @model.name = "Alf" - assert @model.changed? + assert_predicate @model, :changed? @model.save - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? end test "saving should preserve previous changes" do @model.name = "Jericho Cane" + @model.status = "waiting" @model.save assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"] + assert_equal ["initialized", "waiting"], @model.previous_changes["status"] end test "setting new attributes should not affect previous changes" do @model.name = "Jericho Cane" + @model.status = "waiting" @model.save @model.name = "DudeFella ManGuy" + @model.status = "finished" assert_equal [nil, "Jericho Cane"], @model.name_previous_change + assert_equal ["initialized", "waiting"], @model.previous_changes["status"] end test "saving should preserve model's previous changed status" do @model.name = "Jericho Cane" @model.save - assert @model.name_previously_changed? + assert_predicate @model, :name_previously_changed? end test "previous value is preserved when changed after save" do assert_equal({}, @model.changed_attributes) @model.name = "Paul" - assert_equal({ "name" => nil }, @model.changed_attributes) + @model.status = "waiting" + assert_equal({ "name" => nil, "status" => "initialized" }, @model.changed_attributes) @model.save @model.name = "John" - assert_equal({ "name" => "Paul" }, @model.changed_attributes) + @model.status = "finished" + assert_equal({ "name" => "Paul", "status" => "waiting" }, @model.changed_attributes) end test "changing the same attribute multiple times retains the correct original value" do @model.name = "Otto" + @model.status = "waiting" @model.save @model.name = "DudeFella ManGuy" @model.name = "Mr. Manfredgensonton" + @model.status = "processing" + @model.status = "finished" assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change + assert_equal ["waiting", "finished"], @model.status_change assert_equal @model.name_was, "Otto" end test "using attribute_will_change! with a symbol" do @model.size = 1 - assert @model.size_changed? + assert_predicate @model, :size_changed? end test "reload should reset all changes" do @@ -201,7 +222,7 @@ class DirtyTest < ActiveModel::TestCase @model.restore_attributes - assert_not @model.changed? + assert_not_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "Red", @model.color end @@ -215,7 +236,7 @@ class DirtyTest < ActiveModel::TestCase @model.restore_attributes(["name"]) - assert @model.changed? + assert_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "White", @model.color end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index ab18af0de1..fe351f922d 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -83,7 +83,7 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.count person.errors.clear - assert person.errors.empty? + assert_empty person.errors end test "error access is indifferent" do @@ -128,8 +128,8 @@ class ErrorsTest < ActiveModel::TestCase test "detecting whether there are errors with empty?, blank?, include?" do person = Person.new person.errors[:foo] - assert person.errors.empty? - assert person.errors.blank? + assert_empty person.errors + assert_predicate person.errors, :blank? assert_not_includes person.errors, :foo end @@ -223,6 +223,13 @@ class ErrorsTest < ActiveModel::TestCase assert !person.errors.added?(:name) end + test "added? returns false when checking for an error by symbol and a different error with same message is present" do + I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) + person = Person.new + person.errors.add(:name, :wrong) + assert !person.errors.added?(:name, :used) + end + test "size calculates the number of error messages" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -313,7 +320,7 @@ class ErrorsTest < ActiveModel::TestCase test "generate_message works without i18n_scope" do person = Person.new - assert !Person.respond_to?(:i18n_scope) + assert_not_respond_to Person, :i18n_scope assert_nothing_raised { person.errors.generate_message(:name, :blank) } @@ -364,7 +371,7 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.details.count person.errors.clear - assert person.errors.details.empty? + assert_empty person.errors.details end test "copy errors" do diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb index 2d33579595..2de0f53640 100644 --- a/activemodel/test/cases/type/boolean_test.rb +++ b/activemodel/test/cases/type/boolean_test.rb @@ -7,8 +7,8 @@ module ActiveModel class BooleanTest < ActiveModel::TestCase def test_type_cast_boolean type = Type::Boolean.new - assert type.cast("").nil? - assert type.cast(nil).nil? + assert_predicate type.cast(""), :nil? + assert_predicate type.cast(nil), :nil? assert type.cast(true) assert type.cast(1) diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index b91d67f95f..c0cf6ce590 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -7,22 +7,22 @@ module ActiveModel class DecimalTest < ActiveModel::TestCase def test_type_cast_decimal type = Decimal.new - assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.cast(123.0) - assert_equal BigDecimal.new("1"), type.cast(:"1") + assert_equal BigDecimal("0"), type.cast(BigDecimal("0")) + assert_equal BigDecimal("123"), type.cast(123.0) + assert_equal BigDecimal("1"), type.cast(:"1") end def test_type_cast_decimal_from_invalid_string type = Decimal.new assert_nil type.cast("") - assert_equal BigDecimal.new("1"), type.cast("1ignore") - assert_equal BigDecimal.new("0"), type.cast("bad1") - assert_equal BigDecimal.new("0"), type.cast("bad") + assert_equal BigDecimal("1"), type.cast("1ignore") + assert_equal BigDecimal("0"), type.cast("bad1") + assert_equal BigDecimal("0"), type.cast("bad") end def test_type_cast_decimal_from_float_with_large_precision type = Decimal.new(precision: ::Float::DIG + 2) - assert_equal BigDecimal.new("123.0"), type.cast(123.0) + assert_equal BigDecimal("123.0"), type.cast(123.0) end def test_type_cast_from_float_with_unspecified_precision @@ -48,7 +48,7 @@ module ActiveModel def test_type_cast_decimal_from_object_responding_to_d value = Object.new def value.to_d - BigDecimal.new("1") + BigDecimal("1") end type = Decimal.new assert_equal BigDecimal("1"), type.cast(value) diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index 801577474a..8bc4f4723a 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -17,16 +17,16 @@ class AbsenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "foo" t.content = "bar" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be blank"], t.errors[:title] assert_equal ["must be blank"], t.errors[:content] t.title = "" t.content = "something" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be blank"], t.errors[:content] assert_equal [], t.errors[:title] t.content = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_absence_of_with_array_arguments @@ -34,7 +34,7 @@ class AbsenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "foo" t.content = "bar" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be blank"], t.errors[:title] assert_equal ["must be blank"], t.errors[:content] end @@ -43,7 +43,7 @@ class AbsenceValidationTest < ActiveModel::TestCase Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes" p = Person.new p.karma = "good" - assert p.invalid? + assert_predicate p, :invalid? assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last end @@ -51,19 +51,19 @@ class AbsenceValidationTest < ActiveModel::TestCase Person.validates_absence_of :karma p = Person.new p.karma = "good" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["must be blank"], p.errors[:karma] p.karma = nil - assert p.valid? + assert_predicate p, :valid? end def test_validates_absence_of_for_ruby_class_with_custom_reader CustomReader.validates_absence_of :karma p = CustomReader.new p[:karma] = "excellent" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["must be blank"], p.errors[:karma] p[:karma] = "" - assert p.valid? + assert_predicate p, :valid? end end diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index c5f54b1868..7662f996ae 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -15,54 +15,54 @@ class AcceptanceValidationTest < ActiveModel::TestCase Topic.validates_acceptance_of(:terms_of_service) t = Topic.new("title" => "We should not be confirmed") - assert t.valid? + assert_predicate t, :valid? end def test_terms_of_service_agreement Topic.validates_acceptance_of(:terms_of_service) t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] t.terms_of_service = "1" - assert t.valid? + assert_predicate t, :valid? end def test_eula Topic.validates_acceptance_of(:eula, message: "must be abided") t = Topic.new("title" => "We should be confirmed", "eula" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be abided"], t.errors[:eula] t.eula = "1" - assert t.valid? + assert_predicate t, :valid? end def test_terms_of_service_agreement_with_accept_value Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.") t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] t.terms_of_service = "I agree." - assert t.valid? + assert_predicate t, :valid? end def test_terms_of_service_agreement_with_multiple_accept_values Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] t.terms_of_service = 1 - assert t.valid? + assert_predicate t, :valid? t.terms_of_service = "I concur." - assert t.valid? + assert_predicate t, :valid? end def test_validates_acceptance_of_for_ruby_class @@ -71,11 +71,11 @@ class AcceptanceValidationTest < ActiveModel::TestCase p = Person.new p.karma = "" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["must be accepted"], p.errors[:karma] p.karma = "1" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -83,6 +83,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase def test_validates_acceptance_of_true Topic.validates_acceptance_of(:terms_of_service) - assert Topic.new(terms_of_service: true).valid? + assert_predicate Topic.new(terms_of_service: true), :valid? end end diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index d3a9a17a05..ff3cf61746 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -59,6 +59,18 @@ class DogValidatorWithOnCondition < Dog def set_after_validation_marker; history << "after_validation_marker" ; end end +class DogValidatorWithOnMultipleCondition < Dog + before_validation :set_before_validation_marker_on_context_a, on: :context_a + before_validation :set_before_validation_marker_on_context_b, on: :context_b + after_validation :set_after_validation_marker_on_context_a, on: :context_a + after_validation :set_after_validation_marker_on_context_b, on: :context_b + + def set_before_validation_marker_on_context_a; history << "before_validation_marker on context_a"; end + def set_before_validation_marker_on_context_b; history << "before_validation_marker on context_b"; end + def set_after_validation_marker_on_context_a; history << "after_validation_marker on context_a" ; end + def set_after_validation_marker_on_context_b; history << "after_validation_marker on context_b" ; end +end + class DogValidatorWithIfCondition < Dog before_validation :set_before_validation_marker1, if: -> { true } before_validation :set_before_validation_marker2, if: -> { false } @@ -98,6 +110,37 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase assert_equal [], d.history end + def test_on_multiple_condition_is_respected_for_validation_with_matching_context + d = DogValidatorWithOnMultipleCondition.new + d.valid?(:context_a) + assert_equal ["before_validation_marker on context_a", "after_validation_marker on context_a"], d.history + + d = DogValidatorWithOnMultipleCondition.new + d.valid?(:context_b) + assert_equal ["before_validation_marker on context_b", "after_validation_marker on context_b"], d.history + + d = DogValidatorWithOnMultipleCondition.new + d.valid?([:context_a, :context_b]) + assert_equal([ + "before_validation_marker on context_a", + "before_validation_marker on context_b", + "after_validation_marker on context_a", + "after_validation_marker on context_b" + ], d.history) + end + + def test_on_multiple_condition_is_respected_for_validation_without_matching_context + d = DogValidatorWithOnMultipleCondition.new + d.valid?(:save) + assert_equal [], d.history + end + + def test_on_multiple_condition_is_respected_for_validation_without_context + d = DogValidatorWithOnMultipleCondition.new + d.valid? + assert_equal [], d.history + end + def test_before_validation_and_after_validation_callbacks_should_be_called d = DogWithMethodCallbacks.new d.valid? diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index caea8b65ef..1704db9a48 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -13,24 +13,24 @@ class ConditionalValidationTest < ActiveModel::TestCase # When the method returns true Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end def test_if_validation_using_array_of_true_methods Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_true]) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end def test_unless_validation_using_array_of_false_methods Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_false, :condition_is_false]) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -38,21 +38,21 @@ class ConditionalValidationTest < ActiveModel::TestCase # When the method returns true Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end def test_if_validation_using_array_of_true_and_false_methods Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_false]) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end def test_unless_validation_using_array_of_true_and_felse_methods Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_true, :condition_is_false]) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end @@ -60,7 +60,7 @@ class ConditionalValidationTest < ActiveModel::TestCase # When the method returns false Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_false) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end @@ -68,8 +68,8 @@ class ConditionalValidationTest < ActiveModel::TestCase # When the method returns false Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_false) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -78,8 +78,8 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: Proc.new { |r| r.content.size > 4 }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -88,7 +88,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: Proc.new { |r| r.content.size > 4 }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end @@ -97,7 +97,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: Proc.new { |r| r.title != "uhohuhoh" }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end @@ -106,23 +106,23 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: Proc.new { |r| r.title != "uhohuhoh" }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end def test_validation_using_conbining_if_true_and_unless_true_conditions Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end def test_validation_using_conbining_if_true_and_unless_false_conditions Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_false) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index 8b2c65289b..8603a8ac5c 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -14,40 +14,40 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title) t = Topic.new(author_name: "Plutarch") - assert t.valid? + assert_predicate t, :valid? t.title_confirmation = "Parallel Lives" - assert t.invalid? + assert_predicate t, :invalid? t.title_confirmation = nil t.title = "Parallel Lives" - assert t.valid? + assert_predicate t, :valid? t.title_confirmation = "Parallel Lives" - assert t.valid? + assert_predicate t, :valid? end def test_title_confirmation Topic.validates_confirmation_of(:title) t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") - assert t.invalid? + assert_predicate t, :invalid? t.title_confirmation = "We should be confirmed" - assert t.valid? + assert_predicate t, :valid? end def test_validates_confirmation_of_with_boolean_attribute Topic.validates_confirmation_of(:approved) t = Topic.new(approved: true, approved_confirmation: nil) - assert t.valid? + assert_predicate t, :valid? t.approved_confirmation = false - assert t.invalid? + assert_predicate t, :invalid? t.approved_confirmation = true - assert t.valid? + assert_predicate t, :valid? end def test_validates_confirmation_of_for_ruby_class @@ -55,12 +55,12 @@ class ConfirmationValidationTest < ActiveModel::TestCase p = Person.new p.karma_confirmation = "None" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation] p.karma = "None" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -77,7 +77,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title) t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] ensure I18n.load_path.replace @old_load_path @@ -122,13 +122,13 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title, case_sensitive: true) t = Topic.new(title: "title", title_confirmation: "Title") - assert t.invalid? + assert_predicate t, :invalid? end def test_title_confirmation_with_case_sensitive_option_false Topic.validates_confirmation_of(:title, case_sensitive: false) t = Topic.new(title: "title", title_confirmation: "Title") - assert t.valid? + assert_predicate t, :valid? end end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index 68d611e904..50bd47065c 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -14,8 +14,8 @@ class ExclusionValidationTest < ActiveModel::TestCase def test_validates_exclusion_of Topic.validates_exclusion_of(:title, in: %w( abe monkey )) - assert Topic.new("title" => "something", "content" => "abc").valid? - assert Topic.new("title" => "monkey", "content" => "abc").invalid? + assert_predicate Topic.new("title" => "something", "content" => "abc"), :valid? + assert_predicate Topic.new("title" => "monkey", "content" => "abc"), :invalid? end def test_validates_exclusion_of_with_formatted_message @@ -24,8 +24,8 @@ class ExclusionValidationTest < ActiveModel::TestCase assert Topic.new("title" => "something", "content" => "abc") t = Topic.new("title" => "monkey") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["option monkey is restricted"], t.errors[:title] end @@ -35,8 +35,8 @@ class ExclusionValidationTest < ActiveModel::TestCase assert Topic.new("title" => "something", "content" => "abc") t = Topic.new("title" => "monkey") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? end def test_validates_exclusion_of_for_ruby_class @@ -44,12 +44,12 @@ class ExclusionValidationTest < ActiveModel::TestCase p = Person.new p.karma = "abe" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is reserved"], p.errors[:karma] p.karma = "Lifo" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -60,26 +60,26 @@ class ExclusionValidationTest < ActiveModel::TestCase t = Topic.new t.title = "elephant" t.author_name = "sikachu" - assert t.invalid? + assert_predicate t, :invalid? t.title = "wasabi" - assert t.valid? + assert_predicate t, :valid? end def test_validates_exclusion_of_with_range Topic.validates_exclusion_of :content, in: ("a".."g") - assert Topic.new(content: "g").invalid? - assert Topic.new(content: "h").valid? + assert_predicate Topic.new(content: "g"), :invalid? + assert_predicate Topic.new(content: "h"), :valid? end def test_validates_exclusion_of_with_time_range Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago - assert Topic.new(created_at: 5.days.ago).invalid? - assert Topic.new(created_at: 3.days.ago).invalid? - assert Topic.new(created_at: 7.days.ago).valid? - assert Topic.new(created_at: 1.day.ago).valid? + assert_predicate Topic.new(created_at: 5.days.ago), :invalid? + assert_predicate Topic.new(created_at: 3.days.ago), :invalid? + assert_predicate Topic.new(created_at: 7.days.ago), :valid? + assert_predicate Topic.new(created_at: 1.day.ago), :valid? end def test_validates_inclusion_of_with_symbol @@ -92,7 +92,7 @@ class ExclusionValidationTest < ActiveModel::TestCase %w(abe) end - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is reserved"], p.errors[:karma] p = Person.new @@ -102,7 +102,7 @@ class ExclusionValidationTest < ActiveModel::TestCase %w() end - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 3ddda2154a..2a7088b3e8 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -5,7 +5,7 @@ require "cases/helper" require "models/topic" require "models/person" -class PresenceValidationTest < ActiveModel::TestCase +class FormatValidationTest < ActiveModel::TestCase def teardown Topic.clear_validators! end @@ -16,22 +16,22 @@ class PresenceValidationTest < ActiveModel::TestCase t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!") assert t.invalid?, "Shouldn't be valid" assert_equal ["is bad data"], t.errors[:title] - assert t.errors[:content].empty? + assert_empty t.errors[:content] t.title = "Validation macros rule!" - assert t.valid? - assert t.errors[:title].empty? + assert_predicate t, :valid? + assert_empty t.errors[:title] assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } end def test_validate_format_with_allow_blank Topic.validates_format_of(:title, with: /\AValidation\smacros \w+!\z/, allow_blank: true) - assert Topic.new("title" => "Shouldn't be valid").invalid? - assert Topic.new("title" => "").valid? - assert Topic.new("title" => nil).valid? - assert Topic.new("title" => "Validation macros rule!").valid? + assert_predicate Topic.new("title" => "Shouldn't be valid"), :invalid? + assert_predicate Topic.new("title" => ""), :valid? + assert_predicate Topic.new("title" => nil), :valid? + assert_predicate Topic.new("title" => "Validation macros rule!"), :valid? end # testing ticket #3142 @@ -42,7 +42,7 @@ class PresenceValidationTest < ActiveModel::TestCase assert t.invalid?, "Shouldn't be valid" assert_equal ["is bad data"], t.errors[:title] - assert t.errors[:content].empty? + assert_empty t.errors[:content] t.title = "-11" assert t.invalid?, "Shouldn't be valid" @@ -58,14 +58,14 @@ class PresenceValidationTest < ActiveModel::TestCase t.title = "1" - assert t.valid? - assert t.errors[:title].empty? + assert_predicate t, :valid? + assert_empty t.errors[:title] end def test_validate_format_with_formatted_message Topic.validates_format_of(:title, with: /\AValid Title\z/, message: "can't be %{value}") t = Topic.new(title: "Invalid title") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be Invalid title"], t.errors[:title] end @@ -114,10 +114,10 @@ class PresenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "digit" t.content = "Pixies" - assert t.invalid? + assert_predicate t, :invalid? t.content = "1234" - assert t.valid? + assert_predicate t, :valid? end def test_validates_format_of_without_lambda @@ -126,10 +126,10 @@ class PresenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "characters" t.content = "1234" - assert t.invalid? + assert_predicate t, :invalid? t.content = "Pixies" - assert t.valid? + assert_predicate t, :valid? end def test_validates_format_of_for_ruby_class @@ -137,12 +137,12 @@ class PresenceValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Pixies" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is invalid"], p.errors[:karma] p.karma = "1234" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 94df0649a9..daad76759f 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -13,61 +13,61 @@ class InclusionValidationTest < ActiveModel::TestCase def test_validates_inclusion_of_range Topic.validates_inclusion_of(:title, in: "aaa".."bbb") - assert Topic.new("title" => "bbc", "content" => "abc").invalid? - assert Topic.new("title" => "aa", "content" => "abc").invalid? - assert Topic.new("title" => "aaab", "content" => "abc").invalid? - assert Topic.new("title" => "aaa", "content" => "abc").valid? - assert Topic.new("title" => "abc", "content" => "abc").valid? - assert Topic.new("title" => "bbb", "content" => "abc").valid? + assert_predicate Topic.new("title" => "bbc", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "aa", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "aaab", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "aaa", "content" => "abc"), :valid? + assert_predicate Topic.new("title" => "abc", "content" => "abc"), :valid? + assert_predicate Topic.new("title" => "bbb", "content" => "abc"), :valid? end def test_validates_inclusion_of_time_range range_begin = 1.year.ago range_end = Time.now Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) - assert Topic.new(title: "aaa", created_at: 2.years.ago).invalid? - assert Topic.new(title: "aaa", created_at: 3.months.ago).valid? - assert Topic.new(title: "aaa", created_at: 37.weeks.from_now).invalid? - assert Topic.new(title: "aaa", created_at: range_begin).valid? - assert Topic.new(title: "aaa", created_at: range_end).valid? + assert_predicate Topic.new(title: "aaa", created_at: 2.years.ago), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 3.months.ago), :valid? + assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.from_now), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid? end def test_validates_inclusion_of_date_range range_begin = 1.year.until(Date.today) range_end = Date.today Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) - assert Topic.new(title: "aaa", created_at: 2.years.until(Date.today)).invalid? - assert Topic.new(title: "aaa", created_at: 3.months.until(Date.today)).valid? - assert Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)).invalid? - assert Topic.new(title: "aaa", created_at: 1.year.until(Date.today)).valid? - assert Topic.new(title: "aaa", created_at: Date.today).valid? - assert Topic.new(title: "aaa", created_at: range_begin).valid? - assert Topic.new(title: "aaa", created_at: range_end).valid? + assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(Date.today)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(Date.today)), :valid? + assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 1.year.until(Date.today)), :valid? + assert_predicate Topic.new(title: "aaa", created_at: Date.today), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid? end def test_validates_inclusion_of_date_time_range range_begin = 1.year.until(DateTime.current) range_end = DateTime.current Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) - assert Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)).invalid? - assert Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)).valid? - assert Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)).invalid? - assert Topic.new(title: "aaa", created_at: range_begin).valid? - assert Topic.new(title: "aaa", created_at: range_end).valid? + assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)), :valid? + assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid? end def test_validates_inclusion_of Topic.validates_inclusion_of(:title, in: %w( a b c d e f g )) - assert Topic.new("title" => "a!", "content" => "abc").invalid? - assert Topic.new("title" => "a b", "content" => "abc").invalid? - assert Topic.new("title" => nil, "content" => "def").invalid? + assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "a b", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => nil, "content" => "def"), :invalid? t = Topic.new("title" => "a", "content" => "I know you are but what am I?") - assert t.valid? + assert_predicate t, :valid? t.title = "uhoh" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is not included in the list"], t.errors[:title] assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) } @@ -81,30 +81,30 @@ class InclusionValidationTest < ActiveModel::TestCase def test_validates_inclusion_of_with_allow_nil Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true) - assert Topic.new("title" => "a!", "content" => "abc").invalid? - assert Topic.new("title" => "", "content" => "abc").invalid? - assert Topic.new("title" => nil, "content" => "abc").valid? + assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => nil, "content" => "abc"), :valid? end def test_validates_inclusion_of_with_formatted_message Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list") - assert Topic.new("title" => "a", "content" => "abc").valid? + assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid? t = Topic.new("title" => "uhoh", "content" => "abc") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["option uhoh is not in the list"], t.errors[:title] end def test_validates_inclusion_of_with_within_option Topic.validates_inclusion_of(:title, within: %w( a b c d e f g )) - assert Topic.new("title" => "a", "content" => "abc").valid? + assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid? t = Topic.new("title" => "uhoh", "content" => "abc") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? end def test_validates_inclusion_of_for_ruby_class @@ -112,12 +112,12 @@ class InclusionValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Lifo" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is not included in the list"], p.errors[:karma] p.karma = "monkey" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -128,10 +128,10 @@ class InclusionValidationTest < ActiveModel::TestCase t = Topic.new t.title = "wasabi" t.author_name = "sikachu" - assert t.invalid? + assert_predicate t, :invalid? t.title = "elephant" - assert t.valid? + assert_predicate t, :valid? end def test_validates_inclusion_of_with_symbol @@ -144,7 +144,7 @@ class InclusionValidationTest < ActiveModel::TestCase %w() end - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is not included in the list"], p.errors[:karma] p = Person.new @@ -154,7 +154,7 @@ class InclusionValidationTest < ActiveModel::TestCase %w(Lifo) end - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 42f76f3e3c..774a2cde74 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -13,153 +13,153 @@ class LengthValidationTest < ActiveModel::TestCase def test_validates_length_of_with_allow_nil Topic.validates_length_of(:title, is: 5, allow_nil: true) - assert Topic.new("title" => "ab").invalid? - assert Topic.new("title" => "").invalid? - assert Topic.new("title" => nil).valid? - assert Topic.new("title" => "abcde").valid? + assert_predicate Topic.new("title" => "ab"), :invalid? + assert_predicate Topic.new("title" => ""), :invalid? + assert_predicate Topic.new("title" => nil), :valid? + assert_predicate Topic.new("title" => "abcde"), :valid? end def test_validates_length_of_with_allow_blank Topic.validates_length_of(:title, is: 5, allow_blank: true) - assert Topic.new("title" => "ab").invalid? - assert Topic.new("title" => "").valid? - assert Topic.new("title" => nil).valid? - assert Topic.new("title" => "abcde").valid? + assert_predicate Topic.new("title" => "ab"), :invalid? + assert_predicate Topic.new("title" => ""), :valid? + assert_predicate Topic.new("title" => nil), :valid? + assert_predicate Topic.new("title" => "abcde"), :valid? end def test_validates_length_of_using_minimum Topic.validates_length_of :title, minimum: 5 t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "not" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] t.title = "" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] t.title = nil - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] end def test_validates_length_of_using_maximum_should_allow_nil Topic.validates_length_of :title, maximum: 10 t = Topic.new - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_minimum Topic.validates_length_of :title, minimum: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_maximum Topic.validates_length_of :title, maximum: 5 t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_maximum Topic.validates_length_of :title, maximum: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_within Topic.validates_length_of(:title, :content, within: 3..5) t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] t.title = nil t.content = nil - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content] t.title = "abe" t.content = "mad" - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_within_with_exclusive_range Topic.validates_length_of(:title, within: 4...10) t = Topic.new("title" => "9 chars!!") - assert t.valid? + assert_predicate t, :valid? t.title = "Now I'm 10" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too long (maximum is 9 characters)"], t.errors[:title] t.title = "Four" - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_within Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true t = Topic.new("title" => "abc", "content" => "abcd") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_is Topic.validates_length_of :title, is: 5 t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title] t.title = "" - assert t.invalid? + assert_predicate t, :invalid? t.title = nil - assert t.invalid? + assert_predicate t, :invalid? end def test_optionally_validates_length_of_using_is Topic.validates_length_of :title, is: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_bignum @@ -187,45 +187,45 @@ class LengthValidationTest < ActiveModel::TestCase def test_validates_length_of_custom_errors_for_minimum_with_message Topic.validates_length_of(:title, minimum: 5, message: "boo %{count}") t = Topic.new("title" => "uhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["boo 5"], t.errors[:title] end def test_validates_length_of_custom_errors_for_minimum_with_too_short Topic.validates_length_of(:title, minimum: 5, too_short: "hoo %{count}") t = Topic.new("title" => "uhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors[:title] end def test_validates_length_of_custom_errors_for_maximum_with_message Topic.validates_length_of(:title, maximum: 5, message: "boo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["boo 5"], t.errors[:title] end def test_validates_length_of_custom_errors_for_in Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 10"], t.errors["title"] t = Topic.new("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 20"], t.errors["title"] end def test_validates_length_of_custom_errors_for_maximum_with_too_long Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -233,29 +233,29 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: "too short", too_long: "too long" t = Topic.new(title: "a") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["too short"], t.errors["title"] t = Topic.new(title: "aaaaaa") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["too long"], t.errors["title"] end def test_validates_length_of_custom_errors_for_is_with_message Topic.validates_length_of(:title, is: 5, message: "boo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["boo 5"], t.errors["title"] end def test_validates_length_of_custom_errors_for_is_with_wrong_length Topic.validates_length_of(:title, is: 5, wrong_length: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -263,11 +263,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, minimum: 5 t = Topic.new("title" => "一二三四五", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "一二三四" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] end @@ -275,11 +275,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, maximum: 5 t = Topic.new("title" => "一二三四五", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "一二34五六" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"] end @@ -287,12 +287,12 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, :content, within: 3..5) t = Topic.new("title" => "一二", "content" => "12三四五六七") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] t.title = "一二三" t.content = "12三" - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_within_utf8 @@ -312,11 +312,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, is: 5 t = Topic.new("title" => "一二345", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "一二345六" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"] end @@ -324,11 +324,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of(:approved, is: 4) t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1) - assert t.invalid? - assert t.errors[:approved].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:approved], :any? t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234) - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_for_ruby_class @@ -336,12 +336,12 @@ class LengthValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Pix" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma] p.karma = "The Smiths" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -350,80 +350,80 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, within: 5..Float::INFINITY) t = Topic.new("title" => "1234") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? t.title = "12345" - assert t.valid? + assert_predicate t, :valid? Topic.validates_length_of(:author_name, maximum: Float::INFINITY) - assert t.valid? + assert_predicate t, :valid? t.author_name = "A very long author name that should still be valid." * 100 - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed Topic.validates_length_of :title, maximum: 10, allow_nil: false t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? end def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed Topic.validates_length_of :title, maximum: 10, allow_blank: false t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? t.title = "" - assert t.invalid? + assert_predicate t, :invalid? end def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil Topic.validates_length_of :title, minimum: 5, maximum: 10 t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? end def test_validates_length_of_using_minimum_0_should_not_allow_nil Topic.validates_length_of :title, minimum: 0 t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_is_0_should_not_allow_nil Topic.validates_length_of :title, is: 0 t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_with_diff_in_option Topic.validates_length_of(:title, is: 5) Topic.validates_length_of(:title, is: 5, if: Proc.new { false }) - assert Topic.new("title" => "david").valid? - assert Topic.new("title" => "david2").invalid? + assert_predicate Topic.new("title" => "david"), :valid? + assert_predicate Topic.new("title" => "david2"), :invalid? end def test_validates_length_of_using_proc_as_maximum Topic.validates_length_of :title, maximum: ->(model) { 5 } t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_proc_as_maximum_with_model_method @@ -431,14 +431,14 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, maximum: Proc.new(&:max_title_length) t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] t.title = "" - assert t.valid? + assert_predicate t, :valid? end end diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index fbbaf8a30c..01b78ae72e 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -20,7 +20,7 @@ class NumericalityValidationTest < ActiveModel::TestCase INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS INTEGERS = [0, 10, -10] + INTEGER_STRINGS - BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } + BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal(bd) } JUNK = ["not a number", "42 not a number", "0xdeadbeef", "0xinvalidhex", "0Xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"] INFINITY = [1.0 / 0.0] @@ -81,10 +81,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_greater_than_using_differing_numeric_types - Topic.validates_numericality_of :approved, greater_than: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18") - invalid!([-97.18, BigDecimal.new("97.18"), BigDecimal("-97.18")], "must be greater than 97.18") - valid!([97.19, 98, BigDecimal.new("98"), BigDecimal.new("97.19")]) + invalid!([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18") + valid!([97.19, 98, BigDecimal("98"), BigDecimal("97.19")]) end def test_validates_numericality_with_greater_than_using_string_value @@ -102,10 +102,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_greater_than_or_equal_using_differing_numeric_types - Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal("97.18") - invalid!([-97.18, 97.17, 97, BigDecimal.new("97.17"), BigDecimal.new("-97.18")], "must be greater than or equal to 97.18") - valid!([97.18, 98, BigDecimal.new("97.19")]) + invalid!([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18") + valid!([97.18, 98, BigDecimal("97.19")]) end def test_validates_numericality_with_greater_than_or_equal_using_string_value @@ -123,10 +123,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_equal_to_using_differing_numeric_types - Topic.validates_numericality_of :approved, equal_to: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18") invalid!([-97.18], "must be equal to 97.18") - valid!([BigDecimal.new("97.18")]) + valid!([BigDecimal("97.18")]) end def test_validates_numericality_with_equal_to_using_string_value @@ -144,10 +144,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_less_than_using_differing_numeric_types - Topic.validates_numericality_of :approved, less_than: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18") - invalid!([97.18, BigDecimal.new("97.18")], "must be less than 97.18") - valid!([-97.0, 97.0, -97, 97, BigDecimal.new("-97"), BigDecimal.new("97")]) + invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18") + valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")]) end def test_validates_numericality_with_less_than_using_string_value @@ -165,10 +165,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_less_than_or_equal_to_using_differing_numeric_types - Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal("97.18") invalid!([97.19, 98], "must be less than or equal to 97.18") - valid!([-97.18, BigDecimal.new("-97.18"), BigDecimal.new("97.18")]) + valid!([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")]) end def test_validates_numericality_with_less_than_or_equal_using_string_value @@ -237,13 +237,13 @@ class NumericalityValidationTest < ActiveModel::TestCase Topic.validates_numericality_of :approved, less_than: 4, message: "smaller than %{count}" topic = Topic.new("title" => "numeric test", "approved" => 10) - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["smaller than 4"], topic.errors[:approved] Topic.validates_numericality_of :approved, greater_than: 4, message: "greater than %{count}" topic = Topic.new("title" => "numeric test", "approved" => 1) - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["greater than 4"], topic.errors[:approved] end @@ -252,12 +252,12 @@ class NumericalityValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Pix" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is not a number"], p.errors[:karma] p.karma = "1234" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -268,7 +268,7 @@ class NumericalityValidationTest < ActiveModel::TestCase topic = Topic.new topic.approved = (base + 1).to_s - assert topic.invalid? + assert_predicate topic, :invalid? end def test_validates_numericality_with_invalid_args diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index 22c2f0af87..c3eca41070 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -17,25 +17,25 @@ class PresenceValidationTest < ActiveModel::TestCase Topic.validates_presence_of(:title, :content) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:title] assert_equal ["can't be blank"], t.errors[:content] t.title = "something" t.content = " " - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:content] t.content = "like stuff" - assert t.valid? + assert_predicate t, :valid? end def test_accepts_array_arguments Topic.validates_presence_of %w(title content) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:title] assert_equal ["can't be blank"], t.errors[:content] end @@ -43,7 +43,7 @@ class PresenceValidationTest < ActiveModel::TestCase def test_validates_acceptance_of_with_custom_error_using_quotes Person.validates_presence_of :karma, message: "This string contains 'single' and \"double\" quotes" p = Person.new - assert p.invalid? + assert_predicate p, :invalid? assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last end @@ -51,24 +51,24 @@ class PresenceValidationTest < ActiveModel::TestCase Person.validates_presence_of :karma p = Person.new - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["can't be blank"], p.errors[:karma] p.karma = "Cold" - assert p.valid? + assert_predicate p, :valid? end def test_validates_presence_of_for_ruby_class_with_custom_reader CustomReader.validates_presence_of :karma p = CustomReader.new - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["can't be blank"], p.errors[:karma] p[:karma] = "Cold" - assert p.valid? + assert_predicate p, :valid? end def test_validates_presence_of_with_allow_nil_option @@ -78,7 +78,7 @@ class PresenceValidationTest < ActiveModel::TestCase assert t.valid?, t.errors.full_messages t.title = "" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:title] t.title = " " diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 7f32f5dc74..80c347703a 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -37,7 +37,7 @@ class ValidatesTest < ActiveModel::TestCase person = Person.new person.title = 123 - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_built_in_validation_and_options @@ -71,7 +71,7 @@ class ValidatesTest < ActiveModel::TestCase def test_validates_with_if_as_shared_conditions Person.validates :karma, presence: true, email: true, if: :condition_is_false person = Person.new - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_unless_as_local_conditions @@ -84,40 +84,40 @@ class ValidatesTest < ActiveModel::TestCase def test_validates_with_unless_shared_conditions Person.validates :karma, presence: true, email: true, unless: :condition_is_true person = Person.new - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_allow_nil_shared_conditions Person.validates :karma, length: { minimum: 20 }, email: true, allow_nil: true person = Person.new - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_regexp Person.validates :karma, format: /positive|negative/ person = Person.new - assert person.invalid? + assert_predicate person, :invalid? assert_equal ["is invalid"], person.errors[:karma] person.karma = "positive" - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_array Person.validates :gender, inclusion: %w(m f) person = Person.new - assert person.invalid? + assert_predicate person, :invalid? assert_equal ["is not included in the list"], person.errors[:gender] person.gender = "m" - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_range Person.validates :karma, length: 6..20 person = Person.new - assert person.invalid? + assert_predicate person, :invalid? assert_equal ["is too short (minimum is 6 characters)"], person.errors[:karma] person.karma = "something" - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_validator_class_and_options @@ -159,7 +159,7 @@ class ValidatesTest < ActiveModel::TestCase topic = Topic.new topic.title = "What's happening" topic.title_confirmation = "Not this" - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["Y U NO CONFIRM"], topic.errors[:title_confirmation] end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 13ef5e6a31..8239792c79 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -65,7 +65,7 @@ class ValidatesWithTest < ActiveModel::TestCase test "with multiple classes" do Topic.validates_with(ValidatorThatAddsErrors, OtherValidatorThatAddsErrors) topic = Topic.new - assert topic.invalid? + assert_predicate topic, :invalid? assert_includes topic.errors[:base], ERROR_MESSAGE assert_includes topic.errors[:base], OTHER_ERROR_MESSAGE end @@ -79,21 +79,21 @@ class ValidatesWithTest < ActiveModel::TestCase validator.expect(:is_a?, false, [String]) Topic.validates_with(validator, if: :condition_is_true, foo: :bar) - assert topic.valid? + assert_predicate topic, :valid? validator.verify end test "validates_with with options" do Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name) topic = Topic.new - assert topic.invalid? + assert_predicate topic, :invalid? assert_includes topic.errors[:base], ERROR_MESSAGE end test "validates_with each validator" do Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content]) topic = Topic.new title: "Title", content: "Content" - assert topic.invalid? + assert_predicate topic, :invalid? assert_equal ["Value is Title"], topic.errors[:title] assert_equal ["Value is Content"], topic.errors[:content] end @@ -113,28 +113,28 @@ class ValidatesWithTest < ActiveModel::TestCase test "each validator skip nil values if :allow_nil is set to true" do Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_nil: true) topic = Topic.new content: "" - assert topic.invalid? - assert topic.errors[:title].empty? + assert_predicate topic, :invalid? + assert_empty topic.errors[:title] assert_equal ["Value is "], topic.errors[:content] end test "each validator skip blank values if :allow_blank is set to true" do Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_blank: true) topic = Topic.new content: "" - assert topic.valid? - assert topic.errors[:title].empty? - assert topic.errors[:content].empty? + assert_predicate topic, :valid? + assert_empty topic.errors[:title] + assert_empty topic.errors[:content] end test "validates_with can validate with an instance method" do Topic.validates :title, with: :my_validation topic = Topic.new title: "foo" - assert topic.valid? - assert topic.errors[:title].empty? + assert_predicate topic, :valid? + assert_empty topic.errors[:title] topic = Topic.new - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["is missing"], topic.errors[:title] end @@ -142,8 +142,8 @@ class ValidatesWithTest < ActiveModel::TestCase Topic.validates :title, :content, with: :my_validation_with_arg topic = Topic.new title: "foo" - assert !topic.valid? - assert topic.errors[:title].empty? + assert_not_predicate topic, :valid? + assert_empty topic.errors[:title] assert_equal ["is missing"], topic.errors[:content] end end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index ab8c41bbd0..7776233db5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -30,7 +30,7 @@ class ValidationsTest < ActiveModel::TestCase def test_single_attr_validation_and_error_msg r = Reply.new r.title = "There's no content!" - assert r.invalid? + assert_predicate r, :invalid? assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid" assert_equal ["is Empty"], r.errors["content"], "A reply without content should contain an error" assert_equal 1, r.errors.count @@ -38,7 +38,7 @@ class ValidationsTest < ActiveModel::TestCase def test_double_attr_validation_and_error_msg r = Reply.new - assert r.invalid? + assert_predicate r, :invalid? assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid" assert_equal ["is Empty"], r.errors["title"], "A reply without title should contain an error" @@ -111,8 +111,8 @@ class ValidationsTest < ActiveModel::TestCase def test_errors_empty_after_errors_on_check t = Topic.new - assert t.errors[:id].empty? - assert t.errors.empty? + assert_empty t.errors[:id] + assert_empty t.errors end def test_validates_each @@ -122,7 +122,7 @@ class ValidationsTest < ActiveModel::TestCase hits += 1 end t = Topic.new("title" => "valid", "content" => "whatever") - assert t.invalid? + assert_predicate t, :invalid? assert_equal 4, hits assert_equal %w(gotcha gotcha), t.errors[:title] assert_equal %w(gotcha gotcha), t.errors[:content] @@ -135,7 +135,7 @@ class ValidationsTest < ActiveModel::TestCase hits += 1 end t = CustomReader.new("title" => "valid", "content" => "whatever") - assert t.invalid? + assert_predicate t, :invalid? assert_equal 4, hits assert_equal %w(gotcha gotcha), t.errors[:title] assert_equal %w(gotcha gotcha), t.errors[:content] @@ -146,16 +146,16 @@ class ValidationsTest < ActiveModel::TestCase def test_validate_block Topic.validate { errors.add("title", "will never be valid") } t = Topic.new("title" => "Title", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["will never be valid"], t.errors["title"] end def test_validate_block_with_params Topic.validate { |topic| topic.errors.add("title", "will never be valid") } t = Topic.new("title" => "Title", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["will never be valid"], t.errors["title"] end @@ -214,7 +214,7 @@ class ValidationsTest < ActiveModel::TestCase def test_errors_conversions Topic.validates_presence_of %w(title content) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? xml = t.errors.to_xml assert_match %r{<errors>}, xml @@ -232,14 +232,14 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates_length_of :title, minimum: 2 t = Topic.new("title" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal "can't be blank", t.errors["title"].first Topic.validates_presence_of :title, :author_name Topic.validate { errors.add("author_email_address", "will never be valid") } Topic.validates_length_of :title, :content, minimum: 2 t = Topic.new title: "" - assert t.invalid? + assert_predicate t, :invalid? assert_equal :title, key = t.errors.keys[0] assert_equal "can't be blank", t.errors[key][0] @@ -258,8 +258,8 @@ class ValidationsTest < ActiveModel::TestCase t = Topic.new(title: "") # If block should not fire - assert t.valid? - assert t.author_name.nil? + assert_predicate t, :valid? + assert_predicate t.author_name, :nil? # If block should fire assert t.invalid?(:update) @@ -270,18 +270,18 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates_presence_of :title t = Topic.new - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? t.title = "Things are going to change" - assert !t.invalid? + assert_not_predicate t, :invalid? end def test_validation_with_message_as_proc Topic.validates_presence_of(:title, message: proc { "no blanks here".upcase }) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["NO BLANKS HERE"], t.errors[:title] end @@ -331,13 +331,13 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates :content, length: { minimum: 10 } topic = Topic.new - assert topic.invalid? + assert_predicate topic, :invalid? assert_equal 3, topic.errors.size topic.title = "Some Title" topic.author_name = "Some Author" topic.content = "Some Content Whose Length is more than 10." - assert topic.valid? + assert_predicate topic, :valid? end def test_validate @@ -381,7 +381,7 @@ class ValidationsTest < ActiveModel::TestCase def test_strict_validation_not_fails Topic.validates :title, strict: true, presence: true - assert Topic.new(title: "hello").valid? + assert_predicate Topic.new(title: "hello"), :valid? end def test_strict_validation_particular_validator @@ -414,7 +414,7 @@ class ValidationsTest < ActiveModel::TestCase def test_validates_with_false_hash_value Topic.validates :title, presence: false - assert Topic.new.valid? + assert_predicate Topic.new, :valid? end def test_strict_validation_error_message @@ -439,19 +439,19 @@ class ValidationsTest < ActiveModel::TestCase duped = topic.dup duped.title = nil - assert duped.invalid? + assert_predicate duped, :invalid? topic.title = nil duped.title = "Mathematics" - assert topic.invalid? - assert duped.valid? + assert_predicate topic, :invalid? + assert_predicate duped, :valid? end def test_validation_with_message_as_proc_that_takes_a_record_as_a_parameter Topic.validates_presence_of(:title, message: proc { |record| "You have failed me for the last time, #{record.author_name}." }) t = Topic.new(author_name: "Admiral") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["You have failed me for the last time, Admiral."], t.errors[:title] end @@ -459,7 +459,7 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates_presence_of(:title, message: proc { |record, data| "#{data[:attribute]} is missing. You have failed me for the last time, #{record.author_name}." }) t = Topic.new(author_name: "Admiral") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["Title is missing. You have failed me for the last time, Admiral."], t.errors[:title] end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 34ef5c79e1..d73c0859ee 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,384 +1,3 @@ -* Fix `bin/rails db:migrate` with specified `VERSION`. - `bin/rails db:migrate` with empty VERSION behaves as without `VERSION`. - Check a format of `VERSION`: Allow a migration version number - or name of a migration file. Raise error if format of `VERSION` is invalid. - Raise error if target migration doesn't exist. - *bogdanvlviv* -* Fixed a bug where column orders for an index weren't written to - db/schema.rb when using the sqlite adapter. - - Fixes #30902. - - *Paul Kuruvilla* - -* Remove deprecated method `#sanitize_conditions`. - - *Rafael Mendonça França* - -* Remove deprecated method `#scope_chain`. - - *Rafael Mendonça França* - -* Remove deprecated configuration `.error_on_ignored_order_or_limit`. - - *Rafael Mendonça França* - -* Remove deprecated arguments from `#verify!`. - - *Rafael Mendonça França* - -* Remove deprecated argument `name` from `#indexes`. - - *Rafael Mendonça França* - -* Remove deprecated method `ActiveRecord::Migrator.schema_migrations_table_name`. - - *Rafael Mendonça França* - -* Remove deprecated method `supports_primary_key?`. - - *Rafael Mendonça França* - -* Remove deprecated method `supports_migrations?`. - - *Rafael Mendonça França* - -* Remove deprecated methods `initialize_schema_migrations_table` and `initialize_internal_metadata_table`. - - *Rafael Mendonça França* - -* Raises when calling `lock!` in a dirty record. - - *Rafael Mendonça França* - -* Remove deprecated support to passing a class to `:class_name` on associations. - - *Rafael Mendonça França* - -* Remove deprecated argument `default` from `index_name_exists?`. - - *Rafael Mendonça França* - -* Remove deprecated support to `quoted_id` when typecasting an Active Record object. - - *Rafael Mendonça França* - -* Fix `bin/rails db:setup` and `bin/rails db:test:prepare` create wrong - ar_internal_metadata's data for a test database. - - Before: - ``` - $ RAILS_ENV=test rails dbconsole - > SELECT * FROM ar_internal_metadata; - key|value|created_at|updated_at - environment|development|2017-09-11 23:14:10.815679|2017-09-11 23:14:10.815679 - ``` - - After: - ``` - $ RAILS_ENV=test rails dbconsole - > SELECT * FROM ar_internal_metadata; - key|value|created_at|updated_at - environment|test|2017-09-11 23:14:10.815679|2017-09-11 23:14:10.815679 - ``` - - Fixes #26731. - - *bogdanvlviv* - -* Fix longer sequence name detection for serial columns. - - Fixes #28332. - - *Ryuta Kamizono* - -* MySQL: Don't lose `auto_increment: true` in the `db/schema.rb`. - - Fixes #30894. - - *Ryuta Kamizono* - -* Fix `COUNT(DISTINCT ...)` for `GROUP BY` with `ORDER BY` and `LIMIT`. - - Fixes #30886. - - *Ryuta Kamizono* - -* PostgreSQL `tsrange` now preserves subsecond precision. - - PostgreSQL 9.1+ introduced range types, and Rails added support for using - this datatype in Active Record. However, the serialization of - `PostgreSQL::OID::Range` was incomplete, because it did not properly - cast the bounds that make up the range. This led to subseconds being - dropped in SQL commands: - - Before: - - connection.type_cast(tsrange.serialize(range_value)) - # => "[2010-01-01 13:30:00 UTC,2011-02-02 19:30:00 UTC)" - - Now: - - connection.type_cast(tsrange.serialize(range_value)) - # => "[2010-01-01 13:30:00.670277,2011-02-02 19:30:00.745125)" - - *Thomas Cannon* - -* Passing a `Set` to `Relation#where` now behaves the same as passing an - array. - - *Sean Griffin* - -* Use given algorithm while removing index from database. - - Fixes #24190. - - *Mehmet Emin İNAÇ* - -* Update payload names for `sql.active_record` instrumentation to be - more descriptive. - - Fixes #30586. - - *Jeremy Green* - -* Add new error class `TransactionTimeout` which will be raised - when lock wait timeout exceeded. - - *Gabriel Courtemanche* - -* Remove deprecated `#migration_keys`. - - *Ryuta Kamizono* - -* Automatically guess the inverse associations for STI. - - *Yuichiro Kaneko* - -* Ensure `sum` honors `distinct` on `has_many :through` associations - - Fixes #16791. - - *Aaron Wortham* - -* Add `binary` fixture helper method. - - *Atsushi Yoshida* - -* When using `Relation#or`, extract the common conditions and put them before the OR condition. - - *Maxime Handfield Lapointe* - -* `Relation#or` now accepts two relations who have different values for - `references` only, as `references` can be implicitly called by `where`. - - Fixes #29411. - - *Sean Griffin* - -* `ApplicationRecord` is no longer generated when generating models. If you - need to generate it, it can be created with `rails g application_record`. - - *Lisa Ugray* - -* Fix `COUNT(DISTINCT ...)` with `ORDER BY` and `LIMIT` to keep the existing select list. - - *Ryuta Kamizono* - -* When a `has_one` association is destroyed by `dependent: destroy`, - `destroyed_by_association` will now be set to the reflection, matching the - behaviour of `has_many` associations. - - *Lisa Ugray* - -* Fix `unscoped(where: [columns])` removing the wrong bind values - - When the `where` is called on a relation after a `or`, unscoping the column of that later `where` removed - bind values used by the `or` instead. (possibly other cases too) - - ``` - Post.where(id: 1).or(Post.where(id: 2)).where(foo: 3).unscope(where: :foo).to_sql - # Currently: - # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 2 OR "posts"."id" = 3) - # With fix: - # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 1 OR "posts"."id" = 2) - ``` - - *Maxime Handfield Lapointe* - -* Values constructed using multi-parameter assignment will now use the - post-type-cast value for rendering in single-field form inputs. - - *Sean Griffin* - -* `Relation#joins` is no longer affected by the target model's - `current_scope`, with the exception of `unscoped`. - - Fixes #29338. - - *Sean Griffin* - -* Change sqlite3 boolean serialization to use 1 and 0 - - SQLite natively recognizes 1 and 0 as true and false, but does not natively - recognize 't' and 'f' as was previously serialized. - - This change in serialization requires a migration of stored boolean data - for SQLite databases, so it's implemented behind a configuration flag - whose default false value is deprecated. - - *Lisa Ugray* - -* Skip query caching when working with batches of records (`find_each`, `find_in_batches`, - `in_batches`). - - Previously, records would be fetched in batches, but all records would be retained in memory - until the end of the request or job. - - *Eugene Kenny* - -* Prevent errors raised by `sql.active_record` notification subscribers from being converted into - `ActiveRecord::StatementInvalid` exceptions. - - *Dennis Taylor* - -* Fix eager loading/preloading association with scope including joins. - - Fixes #28324. - - *Ryuta Kamizono* - -* Fix transactions to apply state to child transactions - - Previously, if you had a nested transaction and the outer transaction was rolledback, the record from the - inner transaction would still be marked as persisted. - - This change fixes that by applying the state of the parent transaction to the child transaction when the - parent transaction is rolledback. This will correctly mark records from the inner transaction as not persisted. - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Deprecate `set_state` method in `TransactionState` - - Deprecated the `set_state` method in favor of setting the state via specific methods. If you need to mark the - state of the transaction you can now use `rollback!`, `commit!` or `nullify!` instead of - `set_state(:rolledback)`, `set_state(:committed)`, or `set_state(nil)`. - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Deprecate delegating to `arel` in `Relation`. - - *Ryuta Kamizono* - -* Fix eager loading to respect `store_full_sti_class` setting. - - *Ryuta Kamizono* - -* Query cache was unavailable when entering the `ActiveRecord::Base.cache` block - without being connected. - - *Tsukasa Oishi* - -* Previously, when building records using a `has_many :through` association, - if the child records were deleted before the parent was saved, they would - still be persisted. Now, if child records are deleted before the parent is saved - on a `has_many :through` association, the child records will not be persisted. - - *Tobias Kraze* - -* Merging two relations representing nested joins no longer transforms the joins of - the merged relation into LEFT OUTER JOIN. Example to clarify: - - ``` - Author.joins(:posts).merge(Post.joins(:comments)) - # Before the change: - #=> SELECT ... FROM authors INNER JOIN posts ON ... LEFT OUTER JOIN comments ON... - - # After the change: - #=> SELECT ... FROM authors INNER JOIN posts ON ... INNER JOIN comments ON... - ``` - - TODO: Add to the Rails 5.2 upgrade guide - - *Maxime Handfield Lapointe* - -* `ActiveRecord::Persistence#touch` does not work well when optimistic locking enabled and - `locking_column`, without default value, is null in the database. - - *bogdanvlviv* - -* Fix destroying existing object does not work well when optimistic locking enabled and - `locking_column` is null in the database. - - *bogdanvlviv* - -* Use bulk INSERT to insert fixtures for better performance. - - *Kir Shatrov* - -* Prevent creation of bind param if casted value is nil. - - *Ryuta Kamizono* - -* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. - - *Ryuta Kamizono* - -* Loading model schema from database is now thread-safe. - - Fixes #28589. - - *Vikrant Chaudhary*, *David Abdemoulaie* - -* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries - in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key - that does not include a timestamp any more. - - NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps - until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+ - - *DHH* - -* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump - - *Rusty Geldmacher*, *Guillermo Iguaran* - -* Add type caster to `RuntimeReflection#alias_name` - - Fixes #28959. - - *Jon Moss* - -* Deprecate `supports_statement_cache?`. - - *Ryuta Kamizono* - -* Raise error `UnknownMigrationVersionError` on the movement of migrations - when the current migration does not exist. - - *bogdanvlviv* - -* Fix `bin/rails db:forward` first migration. - - *bogdanvlviv* - -* Support Descending Indexes for MySQL. - - MySQL 8.0.1 and higher supports descending indexes: `DESC` in an index definition is no longer ignored. - See https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html. - - *Ryuta Kamizono* - -* Fix inconsistency with changed attributes when overriding AR attribute reader. - - *bogdanvlviv* - -* When calling the dynamic fixture accessor method with no arguments, it now returns all fixtures of this type. - Previously this method always returned an empty array. - - *Kevin McPhillips* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index f9e4444f07..cce00cbc3a 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index ba83a9adb2..19650b82ae 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -208,7 +208,7 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 57c82bf469..591c451da5 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -68,7 +68,7 @@ end (Dir["test/cases/**/*_test.rb"].reject { |x| x.include?("/adapters/") } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(Gem.ruby, "-w" , "-Itest", file) + sh(Gem.ruby, "-w", "-Itest", file) end || raise("Failures") end end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 7ad06fe840..8e42a11df4 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -31,5 +31,5 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "activemodel", version - s.add_dependency "arel", "9.0.0.alpha" + s.add_dependency "arel", ">= 9.0" end diff --git a/activerecord/bin/test b/activerecord/bin/test index 83c192531e..9ecf27ce67 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,6 +1,12 @@ #!/usr/bin/env ruby # frozen_string_literal: true +adapter_index = ARGV.index("--adapter") || ARGV.index("-a") +if adapter_index + ARGV.delete_at(adapter_index) + ENV["ARCONN"] = ARGV.delete_at(adapter_index).strip +end + COMPONENT_ROOT = File.expand_path("..", __dir__) require_relative "../../tools/test" @@ -17,4 +23,5 @@ module Minitest end end +Minitest.load_plugins Minitest.extensions.unshift "active_record" diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 5de6503144..b4377ad6be 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e5e89734d2..27a641f05b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -35,7 +35,7 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(amount currency) + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # @@ -177,9 +177,9 @@ module ActiveRecord # # Once a #composed_of relationship is specified for a model, records can be loaded from the database # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago": # - # Customer.where(balance: Money.new(20, "USD")) + # Customer.where(address: Address.new("May Street", "Chicago")) # module ClassMethods # Adds reader and writer methods for manipulating a value object: @@ -212,8 +212,7 @@ module ActiveRecord # # Option examples: # composed_of :temperature, mapping: %w(reading celsius) - # composed_of :balance, class_name: "Money", mapping: %w(balance amount), - # converter: Proc.new { |balance| balance.to_money } + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, allow_nil: true diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 2b0b2864bc..403667fb70 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -2,8 +2,8 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, predicate_builder, association) - super(klass, table, predicate_builder) + def initialize(klass, association) + super(klass) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 661605d3e5..b1cad0d0a4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1061,12 +1061,6 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # There are limitations to <tt>:inverse_of</tt> support: - # - # * does not work with <tt>:through</tt> associations. - # * does not work with <tt>:polymorphic</tt> associations. - # * inverse associations for #belongs_to associations #has_many are ignored. - # # For more information, see the documentation for the +:inverse_of+ option. # # == Deleting from associations @@ -1279,6 +1273,9 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many # association will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the polymorphic association @@ -1352,8 +1349,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_many association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_many association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:extend] # Specifies a module or array of modules that will be extended into the association object returned. @@ -1449,6 +1445,9 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association # will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the polymorphic association @@ -1464,6 +1463,9 @@ module ActiveRecord # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the # source reflection. You can only use a <tt>:through</tt> query through a #has_one # or #belongs_to association on the join model. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:source] # Specifies the source association name used by #has_one <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. @@ -1484,8 +1486,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_one association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_one association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:required] # When set to +true+, the association will also have its presence validated. @@ -1570,6 +1571,9 @@ module ActiveRecord # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key # of "favorite_person_id". + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the association with a "_type" @@ -1619,8 +1623,7 @@ module ActiveRecord # +after_commit+ and +after_rollback+ callbacks are executed. # [:inverse_of] # Specifies the name of the #has_one or #has_many association on the associated - # object that is the inverse of this #belongs_to association. Does not work in - # combination with the <tt>:polymorphic</tt> options. + # object that is the inverse of this #belongs_to association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:optional] # When set to +true+, the association will not have its presence validated. @@ -1789,6 +1792,9 @@ module ActiveRecord # of this class in lower-case and "_id" suffixed. So a Person class that makes # a #has_and_belongs_to_many association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 14881cfe17..4f3893588e 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -30,20 +30,12 @@ module ActiveRecord join.left.scan( /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i ).size - elsif join.respond_to? :left + elsif join.is_a?(Arel::Nodes::Join) join.left.name == name ? 1 : 0 elsif join.is_a?(Hash) join.fetch(name, 0) else - # this branch is reached by two tests: - # - # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 - # with :posts - # - # activerecord/test/cases/associations/eager_test.rb:1133 - # with :comments - # - 0 + raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join" end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ca1f9f1650..364f1fe74f 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -124,7 +124,7 @@ module ActiveRecord # 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, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) + AssociationRelation.create(klass, self).merge!(klass.all) end def extensions @@ -156,9 +156,9 @@ module ActiveRecord reset end - # We can't dump @reflection since it contains the scope proc + # We can't dump @reflection and @through_reflection since it contains the scope proc def marshal_dump - ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } + ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] } [@reflection.name, ivars] end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index ba54cd8f49..bd2012df84 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -12,17 +12,20 @@ module ActiveRecord if record raise_on_type_mismatch!(record) update_counters_on_replace(record) - replace_keys(record) set_inverse_instance(record) @updated = true else decrement_counters - remove_keys end self.target = record end + def target=(record) + replace_keys(record) + super + end + def default(&block) writer(owner.instance_exec(&block)) if reader.nil? end @@ -49,9 +52,9 @@ module ActiveRecord def update_counters(by) if require_counter_update? && foreign_key_present? if target && !stale_target? - target.increment!(reflection.counter_cache_column, by) + target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - klass.update_counters(target_id, reflection.counter_cache_column => by) + klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch]) end end end @@ -78,11 +81,8 @@ module ActiveRecord end def replace_keys(record) - owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) - end - - def remove_keys - owner[reflection.foreign_key] = nil + owner[reflection.foreign_key] = record ? + record._read_attribute(reflection.association_primary_key(record.class)) : nil end def foreign_key_present? 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 4ce3474bd5..55d789c66a 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -13,12 +13,7 @@ module ActiveRecord def replace_keys(record) super - owner[reflection.foreign_type] = record.class.base_class.name - end - - def remove_keys - super - owner[reflection.foreign_type] = nil + owner[reflection.foreign_type] = record ? record.class.base_class.name : nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index ca3032d967..7c69cd65ee 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -104,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}(*args) - association(:#{name}).reader(*args) + def #{name} + association(:#{name}).reader end CODE end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 9904ee4bed..c161454c1a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -114,9 +114,13 @@ module ActiveRecord::Associations::Builder # :nodoc: BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) }} - model.after_save callback.(:saved_changes), if: :saved_changes? - model.after_touch callback.(:changes_to_save) - model.after_destroy callback.(:changes_to_save) + unless reflection.counter_cache_column + model.after_create 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 def self.add_default_callbacks(model, reflection) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index ed215fb22c..443ccaaa72 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -52,11 +52,11 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.association_primary_key_type + primary_key = reflection.association_primary_key + pk_type = klass.type_for_attribute(primary_key) ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - primary_key = reflection.association_primary_key records = klass.where(primary_key => ids).index_by do |r| r.public_send(primary_key) end.values_at(*ids).compact @@ -79,7 +79,13 @@ module ActiveRecord def find(*args) if options[:inverse_of] && loaded? args_flatten = args.flatten - raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? + model = scope.klass + + if args_flatten.blank? + error_message = "Couldn't find #{model.name} without an ID" + raise RecordNotFound.new(error_message, model.name, model.primary_key, args) + end + result = find_by_scan(*args) result_size = Array(result).size diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8b4a48a38c..9a30198b95 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -32,7 +32,7 @@ module ActiveRecord class CollectionProxy < Relation def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table, klass.predicate_builder + super klass extensions = association.extensions extend(*extensions) if extensions.any? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 07c7f28d2d..cf85a87fa7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -63,7 +63,7 @@ module ActiveRecord count = if reflection.has_cached_counter? owner._read_attribute(reflection.counter_cache_column).to_i else - scope.count + scope.count(:all) end # If there's nothing in the database and @target has no new records 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 adbf52b87c..59929b8c4e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,9 +8,7 @@ module ActiveRecord def initialize(owner, reflection) super - - @through_records = {} - @through_association = nil + @through_records = {} end def concat(*records) @@ -50,11 +48,6 @@ module ActiveRecord end private - - def through_association - @through_association ||= owner.association(through_reflection.name) - end - # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # @@ -140,21 +133,15 @@ module ActiveRecord scope = through_association.scope scope.where! construct_join_attributes(*records) + scope = scope.where(through_scope_attributes) case method when :destroy if scope.klass.primary_key - count = scope.destroy_all.length + count = scope.destroy_all.count(&:destroyed?) else scope.each(&:_run_destroy_callbacks) - - arel = scope.arel - - stmt = Arel::DeleteManager.new - stmt.from scope.klass.arel_table - stmt.wheres = arel.constraints - - count = scope.klass.connection.delete(stmt, "SQL") + count = scope.delete_all end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 36746f9115..491282adf7 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -6,17 +6,16 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation #:nodoc: include ThroughAssociation - def replace(record) - create_through_record(record) + def replace(record, save = true) + create_through_record(record, save) self.target = record end private - - def create_through_record(record) + def create_through_record(record, save) ensure_not_nested - through_proxy = owner.association(through_reflection.name) + through_proxy = through_association through_record = through_proxy.load_target if through_record && !record @@ -30,7 +29,7 @@ module ActiveRecord if through_record through_record.update(attributes) - elsif owner.new_record? + elsif owner.new_record? || !save through_proxy.build(attributes) else through_proxy.create(attributes) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index e1087be9b3..59320431ee 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -83,7 +83,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] def preload(records, associations, preload_scope = nil) - records = records.compact + records = Array.wrap(records).compact if records.empty? [] diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 735da152b7..ec6f0df484 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -82,11 +82,11 @@ module ActiveRecord end def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type + @klass.type_for_attribute(association_key_name).type end def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type + @model.type_for_attribute(owner_key_name).type end def load_records(&block) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index bce2a95ce1..5afb0bc068 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -4,9 +4,24 @@ module ActiveRecord module Associations # = Active Record Through Association module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, to: :reflection + delegate :source_reflection, to: :reflection private + def through_reflection + @through_reflection ||= begin + refl = reflection.through_reflection + + while refl.through_reflection? + refl = refl.through_reflection + end + + refl + end + end + + def through_association + @through_association ||= owner.association(through_reflection.name) + end # We merge in these scopes for two reasons: # @@ -38,24 +53,22 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + association_primary_key = source_reflection.association_primary_key(reflection.klass) + + if association_primary_key == reflection.klass.primary_key && !options[:source_type] join_attributes = { source_reflection.name => records } else join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } + source_reflection.foreign_key => records.map(&association_primary_key.to_sym) } end if options[:source_type] - join_attributes[source_reflection.foreign_type] = - records.map { |record| record.class.base_class.name } + join_attributes[source_reflection.foreign_type] = [ options[:source_type] ] end if records.count == 1 - Hash[join_attributes.map { |k, v| [k, v.first] }] + join_attributes.transform_values!(&:first) else join_attributes end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 23d2aef214..c77790dc9a 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -167,6 +167,46 @@ module ActiveRecord end end + # Regexp whitelist. Matches the following: + # "#{table_name}.#{column_name}" + # "#{column_name}" + COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + + # Regexp whitelist. Matches the following: + # "#{table_name}.#{column_name}" + # "#{table_name}.#{column_name} #{direction}" + # "#{column_name}" + # "#{column_name} #{direction}" + COLUMN_NAME_ORDER_WHITELIST = /\A(?:\w+\.)?\w+(?:\s+asc|\s+desc)?\z/i + + def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :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) } + end + + return if unexpected.none? + + if allow_unsafe_raw_sql == :deprecated + ActiveSupport::Deprecation.warn( + "Dangerous query method (method whose arguments are used as raw " \ + "SQL) called with non-attribute argument(s): " \ + "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \ + "arguments will be disallowed in Rails 6.0. This method should " \ + "not be called with user-provided values, such as request " \ + "parameters or model attributes. Known-safe values can be passed " \ + "by wrapping them in Arel.sql()." + ) + else + raise(ActiveRecord::UnknownAttributeReference, + "Query method called with non-attribute argument(s): " + + unexpected.map(&:inspect).join(", ") + ) + end + end + # Returns true if the given attribute exists, otherwise false. # # class Person < ActiveRecord::Base @@ -416,7 +456,7 @@ module ActiveRecord arel_table = self.class.arel_table attribute_names.each do |name| - attrs[arel_table[name]] = typecasted_attribute_value(name) + attrs[arel_table[name]] = _read_attribute(name) end attrs end @@ -443,9 +483,5 @@ module ActiveRecord def pk_attribute?(name) name == self.class.primary_key end - - def typecasted_attribute_value(name) - _read_attribute(name) - end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3de6fe566d..df4c79b0f6 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -32,9 +32,7 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @mutations_from_database = nil end end @@ -114,12 +112,12 @@ module ActiveRecord # Alias for +changed+ def changed_attribute_names_to_save - changes_to_save.keys + mutations_from_database.changed_attribute_names end # Alias for +changed_attributes+ def attributes_in_database - changes_to_save.transform_values(&:first) + mutations_from_database.changed_values end private diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 0b7c9398a8..35150889d9 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -57,7 +57,7 @@ module ActiveRecord # store_listing = StoreListing.new(price_in_cents: '10.1') # # # before - # store_listing.price_in_cents # => BigDecimal.new(10.1) + # store_listing.price_in_cents # => BigDecimal(10.1) # # class StoreListing < ActiveRecord::Base # attribute :price_in_cents, :integer diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 6974cf74f6..a1250c3835 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -436,6 +436,9 @@ module ActiveRecord if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key + if inverse_reflection = reflection.inverse_of + record.association(inverse_reflection.name).loaded! + end end saved = record.save(validate: !autosave) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 9ab2780760..9dbdf845bd 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -232,7 +232,7 @@ module ActiveRecord # # For example: # - # class Topic + # class Topic < ActiveRecord::Base # has_many :children # # after_save :log_children @@ -257,7 +257,7 @@ module ActiveRecord # # For example: # - # class Topic + # class Topic < ActiveRecord::Base # has_many :children # # after_commit :log_children diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 88b398ad45..dfba78614e 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -3,11 +3,11 @@ module ActiveRecord module CollectionCacheKey def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: - query_signature = Digest::MD5.hexdigest(collection.to_sql) + query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql) key = "#{collection.model_name.cache_key}/query-#{query_signature}" - if collection.loaded? - size = collection.size + if collection.loaded? || collection.distinct_value + size = collection.records.size if size > 0 timestamp = collection.max_by(×tamp_column)._read_attribute(timestamp_column) end @@ -15,13 +15,12 @@ module ActiveRecord if collection.eager_loading? collection = collection.send(:apply_join_dependency) end - column_type = type_for_attribute(timestamp_column.to_s) + column_type = type_for_attribute(timestamp_column) column = connection.column_name_from_arel_node(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.spawn - query.select_values = [column] + query = collection.select(column) subquery_alias = "subquery_for_cache_key" subquery_column = "#{subquery_alias}.#{timestamp_column}" subquery = query.arel.as(subquery_alias) 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 0759f4d2b3..c730584902 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -63,15 +63,13 @@ module ActiveRecord # There are several connection-pooling-related options that you can add to # your database connection configuration: # - # * +pool+: number indicating size of connection pool (default 5) - # * +checkout_timeout+: number of seconds to block and wait for a connection - # before giving up and raising a timeout error (default 5 seconds). - # * +reaping_frequency+: frequency in seconds to periodically run the - # Reaper, which attempts to find and recover connections from dead - # threads, which can occur if a programmer forgets to close a - # connection at the end of a thread or a thread dies unexpectedly. - # Regardless of this setting, the Reaper will be invoked before every - # blocking wait. (Default +nil+, which means don't schedule the Reaper). + # * +pool+: maximum number of connections the pool may manage (default 5). + # * +idle_timeout+: number of seconds that a connection will be kept + # unused in the pool before it is automatically disconnected (default + # 300 seconds). Set this to zero to keep connections forever. + # * +checkout_timeout+: number of seconds to wait for a connection to + # become available before giving up and raising a timeout error (default + # 5 seconds). # #-- # Synchronization policy: @@ -82,11 +80,8 @@ module ActiveRecord # * private methods that require being called in a +synchronize+ blocks # are now explicitly documented class ConnectionPool - # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool - # with which it shares a Monitor. But could be a generic Queue. - # - # The Queue in stdlib's 'thread' could replace this class except - # stdlib's doesn't support waiting with a timeout. + # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. class Queue def initialize(lock = Monitor.new) @lock = lock @@ -175,7 +170,7 @@ module ActiveRecord # Removes and returns the head of the queue if possible, or +nil+. def remove - @queue.shift + @queue.pop end # Remove and return the head the queue if the number of @@ -283,12 +278,12 @@ module ActiveRecord end end - # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. - # A reaper instantiated with a +nil+ frequency will never reap the - # connection pool. + # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on + # +pool+. A reaper instantiated with a zero frequency will never reap + # the connection pool. # - # Configure the frequency by setting "reaping_frequency" in your - # database yaml file. + # Configure the frequency by setting +reaping_frequency+ in your database + # yaml file (default 60 seconds). class Reaper attr_reader :pool, :frequency @@ -298,11 +293,12 @@ module ActiveRecord end def run - return unless frequency + return unless frequency && frequency > 0 Thread.new(frequency, pool) { |t, p| loop do sleep t p.reap + p.flush end } end @@ -326,6 +322,10 @@ module ActiveRecord @spec = spec @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 + if @idle_timeout = spec.config.fetch(:idle_timeout, 300) + @idle_timeout = @idle_timeout.to_f + @idle_timeout = nil if @idle_timeout <= 0 + end # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @@ -356,7 +356,10 @@ module ActiveRecord @lock_thread = false - @reaper = Reaper.new(self, spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f) + # +reaping_frequency+ is configurable mostly for historical reasons, but it could + # also be useful if someone wants a very low +idle_timeout+. + reaping_frequency = spec.config.fetch(:reaping_frequency, 60) + @reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f) @reaper.run end @@ -450,6 +453,21 @@ module ActiveRecord disconnect(false) end + # Discards all connections in the pool (even if they're currently + # leased!), along with the pool itself. Any further interaction with the + # pool (except #spec and #schema_cache) is undefined. + # + # See AbstractAdapter#discard! + def discard! # :nodoc: + synchronize do + return if @connections.nil? # already discarded + @connections.each do |conn| + conn.discard! + end + @connections = @available = @thread_cached_conns = nil + end + end + # Clears the cache which maps classes and re-connects connections that # require reloading. # @@ -575,6 +593,35 @@ module ActiveRecord end end + # Disconnect all connections that have been idle for at least + # +minimum_idle+ seconds. Connections currently checked out, or that were + # checked in less than +minimum_idle+ seconds ago, are unaffected. + def flush(minimum_idle = @idle_timeout) + return if minimum_idle.nil? + + idle_connections = synchronize do + @connections.select do |conn| + !conn.in_use? && conn.seconds_idle >= minimum_idle + end.each do |conn| + conn.lease + + @available.delete conn + @connections.delete conn + end + end + + idle_connections.each do |conn| + conn.disconnect! + end + end + + # Disconnect all currently idle connections. Connections currently checked + # out are unaffected. + def flush! + reap + flush(-1) + end + def num_waiting_in_queue # :nodoc: @available.num_waiting end @@ -866,11 +913,31 @@ module ActiveRecord # about the model. The model needs to pass a specification name to the handler, # in order to look up the correct connection pool. class ConnectionHandler + def self.unowned_pool_finalizer(pid_map) # :nodoc: + lambda do |_| + discard_unowned_pools(pid_map) + end + end + + def self.discard_unowned_pools(pid_map) # :nodoc: + pid_map.each do |pid, pools| + pools.values.compact.each(&:discard!) unless pid == Process.pid + end + end + def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| + # Discard the parent's connection pools immediately; we have no need + # of them + ConnectionHandler.discard_unowned_pools(h) + h[k] = Concurrent::Map.new(initial_capacity: 2) end + + # Backup finalizer: if the forked child never needed a pool, the above + # early discard has not occurred + ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool) end def connection_pool_list @@ -924,6 +991,13 @@ module ActiveRecord connection_pool_list.each(&:disconnect!) end + # Disconnects all currently idle connections. + # + # See ConnectionPool#flush! for details. + def flush_idle_connections! + connection_pool_list.each(&:flush!) + end + # Locate the connection of the nearest super class. This can be an # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined @@ -931,9 +1005,7 @@ module ActiveRecord def retrieve_connection(spec_name) #:nodoc: pool = retrieve_connection_pool(spec_name) raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool - conn = pool.connection - raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn - conn + pool.connection end # Returns true if a connection that's accessible to this class has 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 36048bee03..08f3e15a4b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -356,35 +356,33 @@ module ActiveRecord # Inserts a set of fixtures into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixtures(fixtures, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG return if fixtures.empty? - columns = schema_cache.columns_hash(table_name) + execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") + end - values = fixtures.map do |fixture| - fixture = fixture.stringify_keys + def insert_fixtures_set(fixture_set, tables_to_delete = []) + fixture_inserts = fixture_set.map do |table_name, fixtures| + next if fixtures.empty? - unknown_columns = fixture.keys - columns.keys - if unknown_columns.any? - raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) - end + build_fixture_sql(fixtures, table_name) + end.compact - columns.map do |name, column| - if fixture.key?(name) - type = lookup_cast_type_from_column(column) - bind = Relation::QueryAttribute.new(name, fixture[name], type) - with_yaml_fallback(bind.value_for_database) - else - Arel.sql("DEFAULT") + table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) + + disable_referential_integrity do + transaction(requires_new: true) do + total_sql.each do |sql| + execute sql, "Fixtures Load" + yield if block_given? end end end - - table = Arel::Table.new(table_name) - manager = Arel::InsertManager.new - manager.into(table) - columns.each_key { |column| manager.columns << table[column] } - manager.values = manager.create_values_list(values) - execute manager.to_sql, "Fixtures Insert" end def empty_insert_statement_value @@ -416,6 +414,44 @@ module ActiveRecord alias join_to_delete join_to_update private + def default_insert_value(column) + Arel.sql("DEFAULT") + end + + def build_fixture_sql(fixtures, table_name) + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + default_insert_value(column) + end + end + end + + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + + manager.to_sql + end + + def combine_multi_statements(total_sql) + total_sql.join(";\n") + end # Returns a subquery for the given key using the join information. def subquery_for(key, select) 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 bd05fb8f6e..4a191d337c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -95,6 +95,7 @@ module ActiveRecord if options_sql = options[:options] create_sql << " #{options_sql}" end + create_sql end def column_options(o) 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 be2f625d74..0594b4b485 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,7 @@ module ActiveRecord # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes class IndexDefinition # :nodoc: - attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment def initialize( table, name, @@ -14,6 +14,7 @@ module ActiveRecord columns = [], lengths: {}, orders: {}, + opclasses: {}, where: nil, type: nil, using: nil, @@ -23,13 +24,23 @@ module ActiveRecord @name = name @unique = unique @columns = columns - @lengths = lengths - @orders = orders + @lengths = concise_options(lengths) + @orders = concise_options(orders) + @opclasses = concise_options(opclasses) @where = where @type = type @using = using @comment = comment end + + private + def concise_options(options) + if columns.size == options.size && options.values.uniq.size == 1 + options.values.first + else + options + end + end end # Abstract representation of a column definition. Instances of this type @@ -85,6 +96,11 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def validate? + options.fetch(:validate, true) + end + alias validated? validate? + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s @@ -204,6 +220,7 @@ module ActiveRecord :decimal, :float, :integer, + :json, :string, :text, :time, 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 9b7345f7c3..db033db913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -600,7 +600,7 @@ module ActiveRecord # 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. def remove_column(table_name, column_name, type = nil, options = {}) - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end # Changes the column's definition according to the new options. @@ -715,7 +715,7 @@ module ActiveRecord # # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # - # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it). + # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it). # # ====== Creating a partial index # @@ -738,6 +738,28 @@ module ActiveRecord # # Note: only supported by PostgreSQL and MySQL # + # ====== Creating an index with a specific operator class + # + # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops) + # + # generates: + # + # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL + # + # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops }) + # + # generates: + # + # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL + # + # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops) + # + # generates: + # + # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL + # + # Note: only supported by PostgreSQL + # # ====== Creating an index with a specific type # # add_index(:developers, :name, type: :fulltext) @@ -942,6 +964,8 @@ module ActiveRecord # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ + # [<tt>:validate</tt>] + # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+. def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? @@ -1025,8 +1049,8 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| - ActiveRecord::Migrator.parse_migration_filename(file).first.to_i + versions = migration_context.migration_files.map do |file| + migration_context.parse_migration_filename(file).first.to_i end unless migrated.include?(version) @@ -1120,7 +1144,7 @@ module ActiveRecord def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: column_names = index_column_names(column_name) - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" @@ -1173,20 +1197,22 @@ module ActiveRecord end def add_index_sort_order(quoted_columns, **options) - if order = options[:order] - case order - when Hash - order = order.symbolize_keys - quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } - when String - quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } - end + orders = options_for_index_columns(options[:order]) + quoted_columns.each do |name, column| + column << " #{orders[name].upcase}" if orders[name].present? end + end - quoted_columns + def options_for_index_columns(options) + if options.is_a?(Hash) + options.symbolize_keys + else + Hash.new { |hash, column| hash[column] = options } + end end - # Overridden by the MySQL adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths and by + # the PostgreSQL adapter for supporting operator classes. def add_options_for_index_columns(quoted_columns, **options) if supports_index_sort_order? quoted_columns = add_index_sort_order(quoted_columns, options) @@ -1340,6 +1366,20 @@ module ActiveRecord options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + def add_column_for_alter(table_name, column_name, type, options = {}) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + schema_creation.accept(AddColumnDefinition.new(cd)) + end + + def remove_column_for_alter(table_name, column_name, type = nil, options = {}) + "DROP COLUMN #{quote_column_name(column_name)}" + end + + def remove_columns_for_alter(table_name, *column_names) + column_names.map { |column_name| remove_column_for_alter(table_name, column_name) } + end + def insert_versions_sql(versions) sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 345983a655..559f068c39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -5,6 +5,7 @@ require "active_record/connection_adapters/schema_cache" require "active_record/connection_adapters/sql_type_metadata" require "active_record/connection_adapters/abstract/schema_dumper" require "active_record/connection_adapters/abstract/schema_creation" +require "active_support/concurrency/load_interlock_aware_monitor" require "arel/collectors/bind" require "arel/collectors/composite" require "arel/collectors/sql_string" @@ -104,10 +105,11 @@ module ActiveRecord @logger = logger @config = config @pool = nil + @idle_since = Concurrent.monotonic_time @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} @visitor = arel_visitor - @lock = Monitor.new + @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -117,6 +119,14 @@ module ActiveRecord end end + def migrations_paths # :nodoc: + @config[:migrations_paths] || Migrator.migrations_paths + end + + def migration_context # :nodoc: + MigrationContext.new(migrations_paths) + end + class Version include Comparable @@ -163,6 +173,7 @@ module ActiveRecord "Current thread: #{Thread.current}." end + @idle_since = Concurrent.monotonic_time @owner = nil else raise ActiveRecordError, "Cannot expire connection, it is not currently leased." @@ -182,6 +193,12 @@ module ActiveRecord end end + # Seconds since this connection was returned to the pool + def seconds_idle # :nodoc: + return 0 if in_use? + Concurrent.monotonic_time - @idle_since + end + def unprepared_statement old_prepared_statements, @prepared_statements = @prepared_statements, false yield @@ -263,6 +280,11 @@ module ActiveRecord false end + # Does this adapter support creating invalid constraints? + def supports_validate_constraints? + false + end + # Does this adapter support creating foreign key constraints # in the same statement as creating the table? def supports_foreign_keys_in_create? @@ -304,6 +326,11 @@ module ActiveRecord false end + # Does this adapter support foreign/external tables? + def supports_foreign_tables? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -366,6 +393,19 @@ module ActiveRecord reset_transaction end + # Immediately forget this connection ever existed. Unlike disconnect!, + # this will not communicate with the server. + # + # After calling this method, the behavior of all other methods becomes + # undefined. This is called internally just before a forked process gets + # rid of a connection that belonged to its parent. + def discard! + # This should be overridden by concrete adapters. + # + # Prevent @connection's finalizer from touching the socket, or + # otherwise communicating with its server, when it is collected. + end + # Reset the state of this connection, directing the DBMS to clear # transactions and other connection-related server-side state. Usually a # database-dependent operation. @@ -510,12 +550,7 @@ module ActiveRecord end def extract_limit(sql_type) - case sql_type - when /^bigint/i - 8 - when /\((.*)\)/ - $1.to_i - end + $1.to_i if sql_type =~ /\((.*)\)/ end def translate_exception_class(e, sql) 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 bfec6fb784..1fb0bd8d56 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -44,7 +44,7 @@ module ActiveRecord json: { name: "json" }, } - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private def dealloc(stmt) stmt[:stmt].close end @@ -117,11 +117,11 @@ module ActiveRecord end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: - query_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 + query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 end def release_advisory_lock(lock_name) # :nodoc: - query_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 + query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1 end def native_database_types @@ -249,7 +249,7 @@ module ActiveRecord # create_database 'matt_development', charset: :big5 def create_database(name, options = {}) if options[:collation] - execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" else execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" end @@ -292,14 +292,10 @@ module ActiveRecord SQL end - def create_table(table_name, **options) #:nodoc: - super(table_name, options: "ENGINE=InnoDB", **options) - end - def bulk_change_table(table_name, operations) #:nodoc: sqls = operations.flat_map do |command, args| table, arguments = args.shift, args - method = :"#{command}_sql" + method = :"#{command}_for_alter" if respond_to?(method, true) send(method, table, *arguments) @@ -372,11 +368,11 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}") rename_column_indexes(table_name, column_name, new_column_name) end @@ -403,12 +399,13 @@ module ActiveRecord fk.constraint_name AS 'name', rc.update_rule AS 'on_update', rc.delete_rule AS 'on_delete' - FROM information_schema.key_column_usage fk - JOIN information_schema.referential_constraints rc + FROM information_schema.referential_constraints rc + JOIN information_schema.key_column_usage fk USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{scope[:schema]} AND fk.table_name = #{scope[:name]} + AND rc.constraint_schema = #{scope[:schema]} AND rc.table_name = #{scope[:name]} SQL @@ -529,23 +526,49 @@ module ActiveRecord index.using == :btree || super end - def insert_fixtures(*) - without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } + def insert_fixtures_set(fixture_set, tables_to_delete = []) + with_multi_statements do + super { discard_remaining_results } + end end private + def combine_multi_statements(total_sql) + total_sql.each_with_object([]) do |sql, total_sql_chunks| + previous_packet = total_sql_chunks.last + sql << ";\n" + if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty? + total_sql_chunks << sql + else + previous_packet << sql + end + end + end - def without_sql_mode(mode) - result = execute("SELECT @@SESSION.sql_mode") - current_mode = result.first[0] - return yield unless current_mode.include?(mode) + def max_allowed_packet_reached?(current_packet, previous_packet) + if current_packet.bytesize > max_allowed_packet + raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable." + elsif previous_packet.nil? + false + else + (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet + end + end + + def max_allowed_packet + bytes_margin = 2 + @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin) + end + + def with_multi_statements + previous_flags = @config[:flags] + @config[:flags] = Mysql2::Client::MULTI_STATEMENTS + reconnect! - sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") yield ensure - sql_mode = "CONCAT(@@sql_mode, ',#{mode}')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") + @config[:flags] = previous_flags + reconnect! end def initialize_type_map(m = type_map) @@ -605,25 +628,6 @@ module ActiveRecord end end - def add_index_length(quoted_columns, **options) - if length = options[:length] - case length - when Hash - length = length.symbolize_keys - quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } - when Integer - quoted_columns.each { |name, column| column << "(#{length})" } - end - end - - quoted_columns - end - - def add_options_for_index_columns(quoted_columns, **options) - quoted_columns = add_index_length(quoted_columns, options) - super - end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 @@ -635,6 +639,8 @@ module ActiveRecord ER_CANNOT_ADD_FOREIGN = 1215 ER_CANNOT_CREATE_TABLE = 1005 ER_LOCK_WAIT_TIMEOUT = 1205 + ER_QUERY_INTERRUPTED = 1317 + ER_QUERY_TIMEOUT = 3024 def translate_exception(exception, message) case error_number(exception) @@ -659,19 +665,17 @@ module ActiveRecord when ER_LOCK_DEADLOCK Deadlocked.new(message) when ER_LOCK_WAIT_TIMEOUT - TransactionTimeout.new(message) + LockWaitTimeout.new(message) + when ER_QUERY_TIMEOUT + StatementTimeout.new(message) + when ER_QUERY_INTERRUPTED + QueryCanceled.new(message) else super end end - def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition(table_name) - cd = td.new_column_definition(column_name, type, options) - schema_creation.accept(AddColumnDefinition.new(cd)) - end - - def change_column_sql(table_name, column_name, type, options = {}) + def change_column_for_alter(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) type ||= column.sql_type @@ -692,7 +696,7 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def rename_column_sql(table_name, column_name, new_column_name) + def rename_column_for_alter(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { default: column.default, @@ -706,31 +710,23 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def remove_column_sql(table_name, column_name, type = nil, options = {}) - "DROP #{quote_column_name(column_name)}" - end - - def remove_columns_sql(table_name, *column_names) - column_names.map { |column_name| remove_column_sql(table_name, column_name) } - end - - def add_index_sql(table_name, column_name, options = {}) + def add_index_for_alter(table_name, column_name, options = {}) index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) index_algorithm[0, 0] = ", " if index_algorithm.present? "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end - def remove_index_sql(table_name, options = {}) + def remove_index_for_alter(table_name, options = {}) index_name = index_name_for_remove(table_name, options) "DROP INDEX #{quote_column_name(index_name)}" end - def add_timestamps_sql(table_name, options = {}) - [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name, options = {}) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + def remove_timestamps_for_alter(table_name, options = {}) + [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 @@ -743,7 +739,8 @@ module ActiveRecord # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' subselect.distinct unless select.limit || select.offset || select.orders.any? - Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name)) + 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? 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 a058a72872..458c9bfd70 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord else super end - @connection.next_result while @connection.more_results? + discard_remaining_results result end @@ -50,11 +50,18 @@ module ActiveRecord alias :exec_update :exec_delete private + def default_insert_value(column) + Arel.sql("DEFAULT") unless column.auto_increment? + end def last_inserted_id(result) @connection.last_id end + def discard_remaining_results + @connection.abandon_results! + end + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index da25e4863c..2ed4ad16ae 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -32,10 +32,6 @@ module ActiveRecord args.each { |name| column(name, :longtext, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def unsigned_integer(*args, **options) args.each { |name| column(name, :unsigned_integer, options) } 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 a15c7d1787..ce50590651 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -22,23 +22,26 @@ module ActiveRecord index_using = mysql_index_type end - indexes << IndexDefinition.new( + indexes << [ row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, + [], + lengths: {}, + orders: {}, type: index_type, using: index_using, comment: row[:Index_comment].presence - ) + ] end - indexes.last.columns << row[:Column_name] - indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + indexes.last[-2] << row[:Column_name] + indexes.last[-1][:lengths].merge!(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" end end - indexes + indexes.map { |index| IndexDefinition.new(*index) } end def remove_column(table_name, column_name, type = nil, options = {}) @@ -103,6 +106,18 @@ module ActiveRecord super unless specifier == "RESTRICT" end + def add_index_length(quoted_columns, **options) + lengths = options_for_index_columns(options[:length]) + quoted_columns.each do |name, column| + column << "(#{lengths[name]})" if lengths[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8de582fee1..bfdc7995f0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,9 +3,8 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", ">= 0.3.18", "< 0.5" +gem "mysql2", "~> 0.4.4" require "mysql2" -raise "mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+" if Mysql2::VERSION == "0.4.3" module ActiveRecord module ConnectionHandling # :nodoc: @@ -105,6 +104,11 @@ module ActiveRecord @connection.close end + def discard! # :nodoc: + @connection.automatic_close = false + @connection = nil + end + private def connect diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb index 879dba7afd..e7d33855c4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -6,7 +6,7 @@ module ActiveRecord module OID # :nodoc: class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) - BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + BigDecimal("Infinity") * (options[:negative] ? -1 : 1) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index a89aa5ea09..6edb7cfd3c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -60,7 +60,7 @@ module ActiveRecord end def type_cast_single_for_database(value) - infinity?(value) ? "" : @subtype.serialize(value) + infinity?(value) ? value : @subtype.serialize(value) end def extract_bounds(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 9fdeab06c1..e75202b0be 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -138,7 +138,7 @@ module ActiveRecord end def encode_range(range) - "[#{type_cast(range.first)},#{type_cast(range.last)}#{range.exclude_end? ? ')' : ']'}" + "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}" end def determine_encoding_of_strings_in_array(value) @@ -154,6 +154,14 @@ module ActiveRecord else _type_cast(values) end end + + def type_cast_range_value(value) + infinity?(value) ? "" : type_cast(value) + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb index 59f661da25..8e381a92cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -5,6 +5,18 @@ module ActiveRecord module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private + def visit_AlterTable(o) + super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") + end + + def visit_AddForeignKey(o) + super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? } + end + + def visit_ValidateConstraint(name) + "VALIDATE CONSTRAINT #{quote_column_name(name)}" + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 75622eb304..6047217fcd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -95,10 +95,6 @@ module ActiveRecord args.each { |name| column(name, :int8range, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def jsonb(*args, **options) args.each { |name| column(name, :jsonb, options) } end @@ -192,6 +188,19 @@ module ActiveRecord class Table < ActiveRecord::ConnectionAdapters::Table include ColumnMethods end + + class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable + attr_reader :constraint_validations + + def initialize(td) + super + @constraint_validations = [] + end + + def validate_constraint(name) + @constraint_validations << name + end + end end end end 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 846e721983..7999434ad8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -38,7 +38,7 @@ module ActiveRecord " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" - else + else "" end end @@ -87,10 +87,7 @@ module ActiveRecord result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, - pg_catalog.obj_description(i.oid, 'pg_class') AS comment, - (SELECT COUNT(*) FROM pg_opclass o - JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c - ON o.oid = c.oid WHERE o.opcdefault = 'f') + pg_catalog.obj_description(i.oid, 'pg_class') AS comment FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid @@ -109,11 +106,13 @@ module ActiveRecord inddef = row[3] oid = row[4] comment = row[5] - opclass = row[6] using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten - if indkey.include?(0) || opclass > 0 + orders = {} + opclasses = {} + + if indkey.include?(0) columns = expressions else columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact @@ -123,10 +122,16 @@ module ActiveRecord AND a.attnum IN (#{indkey.join(",")}) SQL - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - orders = Hash[ - expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } - ] + # add info on sort order (only desc order is explicitly specified, asc is the default) + # and non-default opclasses + expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls| + opclasses[column] = opclass.to_sym if opclass + if nulls + orders[column] = [desc, nulls].compact.join(" ") + else + orders[column] = :desc if desc + end + end end IndexDefinition.new( @@ -135,6 +140,7 @@ module ActiveRecord unique, columns, orders: orders, + opclasses: opclasses, where: where, using: using.to_sym, comment: comment.presence @@ -362,6 +368,31 @@ module ActiveRecord SQL end + def bulk_change_table(table_name, operations) + sql_fragments = [] + non_combinable_operations = [] + + operations.each do |command, args| + table, arguments = args.shift, args + method = :"#{command}_for_alter" + + if respond_to?(method, true) + sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) } + sql_fragments << sqls + non_combinable_operations.concat(procs) + else + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? + non_combinable_operations.each(&:call) + sql_fragments = [] + non_combinable_operations = [] + send(command, table, *arguments) + end + end + + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? + non_combinable_operations.each(&:call) + end + # Renames a table. # Also renames a table's primary key sequence if the sequence name exists and # matches the Active Record default. @@ -392,50 +423,23 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! - quoted_table_name = quote_table_name(table_name) - quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options) - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - if options[:using] - sql << " USING #{options[:using]}" - elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options) - sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" - end - execute sql - - change_column_default(table_name, column_name, options[:default]) if options.key?(:default) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) } + execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}" + procs.each(&:call) end # Changes the default value of a table column. def change_column_default(table_name, column_name, default_or_changes) # :nodoc: - clear_cache! - column = column_for(table_name, column_name) - return unless column - - default = extract_new_default_value(default_or_changes) - alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" - if default.nil? - # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will - # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". - execute alter_column_query % "DROP DEFAULT" - else - execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" - end + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}" end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}" end # Adds comment for given table column or drops it if +comment+ is a +nil+ @@ -458,8 +462,8 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment end end @@ -499,7 +503,7 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") - SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid JOIN pg_class t2 ON c.confrelid = t2.oid @@ -521,11 +525,20 @@ module ActiveRecord options[:on_delete] = extract_foreign_key_action(row["on_delete"]) options[:on_update] = extract_foreign_key_action(row["on_update"]) + options[:validate] = row["valid"] ForeignKeyDefinition.new(table_name, row["to_table"], options) end end + def foreign_tables + query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA") + end + + def foreign_table_exists?(table_name) + query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present? + end + # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -581,6 +594,43 @@ module ActiveRecord PostgreSQL::SchemaDumper.create(self, options) end + # Validates the given constraint. + # + # Validates the constraint named +constraint_name+ on +accounts+. + # + # validate_constraint :accounts, :constraint_name + def validate_constraint(table_name, constraint_name) + return unless supports_validate_constraints? + + at = create_alter_table table_name + at.validate_constraint constraint_name + + execute schema_creation.accept(at) + end + + # Validates the given foreign key. + # + # Validates the foreign key on +accounts.branch_id+. + # + # validate_foreign_key :accounts, :branches + # + # Validates the foreign key on +accounts.owner_id+. + # + # validate_foreign_key :accounts, column: :owner_id + # + # Validates the foreign key named +special_fk_name+ on the +accounts+ table. + # + # validate_foreign_key :accounts, name: :special_fk_name + # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + def validate_foreign_key(from_table, options_or_to_table = {}) + return unless supports_validate_constraints? + + fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name + + validate_constraint from_table, fk_name_to_validate + end + private def schema_creation PostgreSQL::SchemaCreation.new(self) @@ -590,6 +640,10 @@ module ActiveRecord PostgreSQL::TableDefinition.new(*args) end + def create_alter_table(name) + PostgreSQL::AlterTable.new create_table_definition(name) + end + def new_column_from_field(table_name, field) column_name, type, default, notnull, oid, fmod, collation, comment = field type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) @@ -629,9 +683,75 @@ module ActiveRecord end end + 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 + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + if options[:using] + sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options) + sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" + end + + sql + 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) + sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment) + 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) + return unless column + + default = extract_new_default_value(default_or_changes) + alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + alter_column_query % "DROP DEFAULT" + else + alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" + end + end + + def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc: + "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" + end + + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] + end + + def remove_timestamps_for_alter(table_name, options = {}) + [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] + end + + def add_index_opclass(quoted_columns, **options) + opclasses = options_for_index_columns(options[:opclass]) + quoted_columns.each do |name, column| + column << " #{opclasses[name]}" if opclasses[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_opclass(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + scope[:type] ||= "'r','v','m','f'" # (r)elation/table, (v)iew, (m)aterialized view, (f)oreign table sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup sql << " WHERE n.nspname = #{scope[:schema]}" @@ -648,6 +768,8 @@ module ActiveRecord "'r'" when "VIEW" "'v','m'" + when "FOREIGN TABLE" + "'f'" end scope = {} scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 46863c41ab..dc6287e32c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility -gem "pg", "~> 0.18" +gem "pg", ">= 0.18", "< 2.0" require "pg" require "active_record/connection_adapters/abstract_adapter" @@ -122,6 +122,10 @@ module ActiveRecord include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements + def supports_bulk_alter? + true + end + def supports_index_sort_order? true end @@ -142,6 +146,10 @@ module ActiveRecord true end + def supports_validate_constraints? + true + end + def supports_views? true end @@ -166,7 +174,7 @@ module ActiveRecord { concurrently: "CONCURRENTLY" } end - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: def initialize(connection, max) super(max) @connection = connection @@ -182,7 +190,6 @@ module ActiveRecord end private - def dealloc(key) @connection.query "DEALLOCATE #{key}" if connection_active? rescue PG::Error @@ -273,6 +280,11 @@ module ActiveRecord end end + def discard! # :nodoc: + @connection.socket_io.reopen(IO::NULL) rescue nil + @connection = nil + end + def native_database_types #:nodoc: NATIVE_DATABASE_TYPES end @@ -306,6 +318,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_foreign_tables? + postgresql_version >= 90300 + end + def supports_pgcrypto_uuid? postgresql_version >= 90400 end @@ -382,7 +398,6 @@ module ActiveRecord end private - # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" @@ -392,6 +407,7 @@ module ActiveRecord SERIALIZATION_FAILURE = "40001" DEADLOCK_DETECTED = "40P01" LOCK_NOT_AVAILABLE = "55P03" + QUERY_CANCELED = "57014" def translate_exception(exception, message) return exception unless exception.respond_to?(:result) @@ -412,7 +428,9 @@ module ActiveRecord when DEADLOCK_DETECTED Deadlocked.new(message) when LOCK_NOT_AVAILABLE - TransactionTimeout.new(message) + LockWaitTimeout.new(message) + when QUERY_CANCELED + QueryCanceled.new(message) else super end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index f34b6733da..c29cf1f9a1 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -28,7 +28,7 @@ module ActiveRecord coder["columns_hash"] = @columns_hash coder["primary_keys"] = @primary_keys coder["data_sources"] = @data_sources - coder["version"] = ActiveRecord::Migrator.current_version + coder["version"] = connection.migration_context.current_version end def init_with(coder) @@ -100,7 +100,7 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. - @version = ActiveRecord::Migrator.current_version + @version = connection.migration_context.current_version [@version, @columns, @columns_hash, @primary_keys, @data_sources] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 670afa3684..a958600446 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -60,7 +60,7 @@ module ActiveRecord include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { - primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", + primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL", string: { name: "varchar" }, text: { name: "text" }, integer: { name: "integer" }, @@ -70,7 +70,8 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - boolean: { name: "boolean" } + boolean: { name: "boolean" }, + json: { name: "json" }, } ## @@ -90,9 +91,8 @@ module ActiveRecord # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true class_attribute :represent_boolean_as_integer, default: false - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private - def dealloc(stmt) stmt[:stmt].close unless stmt[:stmt].closed? end @@ -101,7 +101,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @active = nil + @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) configure_connection @@ -135,12 +135,16 @@ module ActiveRecord true end + def supports_json? + true + end + def supports_multi_insert? sqlite_version >= "3.7.11" end def active? - @active != false + @active end # Disconnects from the database if already connected. Otherwise, this @@ -286,19 +290,18 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - # See: https://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_type?(type) - type.to_sym != :primary_key + def valid_alter_table_type?(type, options = {}) + !invalid_alter_table_type?(type, options) end + deprecate :valid_alter_table_type? def add_column(table_name, column_name, type, options = {}) #:nodoc: - if valid_alter_table_type?(type) - super(table_name, column_name, type, options) - else + if invalid_alter_table_type?(type, options) alter_table(table_name) do |definition| definition.column(column_name, type, options) end + else + super end end @@ -364,12 +367,30 @@ module ActiveRecord end def insert_fixtures(rows, table_name) - rows.each do |row| - insert_fixture(row, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG + insert_fixtures_set(table_name => rows) + end + + def insert_fixtures_set(fixture_set, tables_to_delete = []) + disable_referential_integrity do + transaction(requires_new: true) do + tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" } + + fixture_set.each do |table_name, rows| + rows.each { |row| insert_fixture(row, table_name) } + end + end end end private + def initialize_type_map(m = type_map) + super + register_class_with_limit m, %r(int)i, SQLite3Integer + end def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") @@ -378,6 +399,12 @@ module ActiveRecord end alias column_definitions table_structure + # See: https://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def invalid_alter_table_type?(type, options) + type.to_sym == :primary_key || options[:primary_key] + end + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" caller = lambda { |definition| yield definition if block_given? } @@ -399,18 +426,21 @@ module ActiveRecord options[:id] = false create_table(to, options) do |definition| @definition = definition - @definition.primary_key(from_primary_key) if from_primary_key.present? + if from_primary_key.is_a?(Array) + @definition.primary_keys from_primary_key + end columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || options[:rename][column.name.to_sym] || column.name) : column.name - next if column_name == from_primary_key @definition.column(column_name, column.type, limit: column.limit, default: column.default, precision: column.precision, scale: column.scale, - null: column.null, collation: column.collation) + null: column.null, collation: column.collation, + primary_key: column_name == from_primary_key + ) end yield @definition if block_given? end @@ -423,6 +453,9 @@ module ActiveRecord def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name + # indexes sqlite creates for internal use start with `sqlite_` and + # don't need to be copied + next if name.starts_with?("sqlite_") if to == "a#{from}" name = "t#{name}" elsif from == "a#{to}" @@ -438,6 +471,7 @@ module ActiveRecord # index name can't be the same opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true } opts[:unique] = true if index.unique + opts[:where] = index.where if index.where add_index(to, columns, opts) end end @@ -525,6 +559,17 @@ module ActiveRecord def configure_connection execute("PRAGMA foreign_keys = ON", "SCHEMA") end + + class SQLite3Integer < Type::Integer # :nodoc: + private + def _limit + # INTEGER storage class can be stored 8 bytes value. + # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes + limit || 8 + end + end + + ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3) end ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter) end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 9a47edfba4..88d28dc52a 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -140,6 +140,6 @@ module ActiveRecord end delegate :clear_active_connections!, :clear_reloadable_connections!, - :clear_all_connections!, to: :connection_handler + :clear_all_connections!, :flush_idle_connections!, to: :connection_handler end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0f7a503c90..e1a0b2ecf8 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -18,6 +18,13 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## + # :singleton-method: + # + # Specifies if the methods calling database queries should be logged below + # their relevant queries. Defaults to false. + mattr_accessor :verbose_query_logs, instance_writer: false, default: false + + ## # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -76,6 +83,14 @@ module ActiveRecord # scope being ignored is error-worthy, rather than a warning. mattr_accessor :error_on_ignored_order, instance_writer: false, default: false + # :singleton-method: + # Specify the behavior for unsafe raw query methods. Values are as follows + # deprecated - Warnings are logged when unsafe raw SQL is passed to + # query methods. + # disabled - Unsafe raw SQL passed to query methods results in + # UnknownAttributeReference exception. + mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated + ## # :singleton-method: # Specify whether or not to use timestamps for migration versions @@ -266,10 +281,11 @@ module ActiveRecord end def relation - relation = Relation.create(self, arel_table, predicate_builder) + relation = Relation.create(self) if finder_needs_type_condition? && !ignore_default_scope? - relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) + relation.where!(type_condition) + relation.create_with!(inheritance_column.to_s => sti_name) else relation end @@ -366,8 +382,10 @@ module ActiveRecord _run_initialize_callbacks - @new_record = true - @destroyed = false + @new_record = true + @destroyed = false + @_start_transaction_state = {} + @transaction_state = nil super end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f373b98035..1a3e6e4d09 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -221,6 +221,8 @@ module ActiveRecord def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) raise_conflict_error(enum_name, method_name, type: "class") + elsif klass_method && method_defined_within?(method_name, Relation) + raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name) elsif !klass_method && dangerous_attribute_method?(method_name) raise_conflict_error(enum_name, method_name) elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 9ef3316393..efcbd44776 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -335,8 +335,40 @@ module ActiveRecord class IrreversibleOrderError < ActiveRecordError end - # TransactionTimeout will be raised when lock wait timeout expires. - # Wait time value is set by innodb_lock_wait_timeout. - class TransactionTimeout < StatementInvalid + # LockWaitTimeout will be raised when lock wait timeout exceeded. + class LockWaitTimeout < StatementInvalid + end + + # StatementTimeout will be raised when statement timeout exceeded. + class StatementTimeout < StatementInvalid + end + + # QueryCanceled will be raised when canceling statement due to user request. + class QueryCanceled < StatementInvalid + end + + # UnknownAttributeReference is raised when an unknown and potentially unsafe + # value is passed to a query method when allow_unsafe_raw_sql is set to + # :disabled. For example, passing a non column name value to a relation's + # #order method might cause this exception. + # + # When working around this exception, caution should be taken to avoid SQL + # injection vulnerabilities when passing user-provided values to query + # methods. Known-safe values can be passed to query methods by wrapping them + # in Arel.sql. + # + # For example, with allow_unsafe_raw_sql set to :disabled, the following + # code would raise this exception: + # + # Post.order("length(title)").first + # + # The desired result can be accomplished by wrapping the known-safe string + # in Arel.sql: + # + # Post.order(Arel.sql("length(title)")).first + # + # Again, such a workaround should *not* be used when passing user-provided + # values, such as request parameters or model attributes to query methods. + class UnknownAttributeReference < ActiveRecordError end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 86f13d75d5..d9a75d9ad6 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -169,13 +169,13 @@ module ActiveRecord # self.use_transactional_tests = true # # test "godzilla" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # Foo.destroy_all - # assert Foo.all.empty? + # assert_empty Foo.all # end # # test "godzilla aftermath" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # end # end # @@ -540,47 +540,38 @@ module ActiveRecord } unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} - - 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, - klass, - ::File.join(fixtures_directory, fs_name)) - end - - update_all_loaded_fixtures fixtures_map - - connection.transaction(requires_new: true) do - deleted_tables = Hash.new { |h, k| h[k] = Set.new } - fixture_sets.each do |fs| - conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows + fixtures_map = {} + + 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, + klass, + ::File.join(fixtures_directory, fs_name)) + end - table_rows.each_key do |table| - unless deleted_tables[conn].include? table - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" - end - deleted_tables[conn] << table - end + update_all_loaded_fixtures fixtures_map + fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } - table_rows.each do |fixture_set_name, rows| - conn.insert_fixtures(rows, fixture_set_name) - end + fixture_sets_by_connection.each do |conn, set| + table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - # Cap primary key sequences to max(pk). - if conn.respond_to?(:reset_pk_sequence!) - conn.reset_pk_sequence!(fs.table_name) - end + set.each do |fs| + fs.table_rows.each do |table, rows| + table_rows_for_connection[table].unshift(*rows) end end + conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if conn.respond_to?(:reset_pk_sequence!) + 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 diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 7ccb57b305..72035a986b 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -7,8 +7,8 @@ module ActiveRecord end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 0715c11473..f3fe610c09 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -47,14 +47,13 @@ module ActiveRecord # Determines if one of the attributes passed in is the inheritance column, # and if the inheritance column is attr accessible, it initializes an # instance of the given subclass instead of the base class. - def new(*args, &block) + def new(attributes = nil, &block) if abstract_class? || self == Base raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." end - attrs = args.first if has_attribute?(inheritance_column) - subclass = subclass_from_attributes(attrs) + subclass = subclass_from_attributes(attributes) if subclass.nil? && base_class == self subclass = subclass_from_attributes(column_defaults) @@ -62,7 +61,7 @@ module ActiveRecord end if subclass && subclass != self - subclass.new(*args, &block) + subclass.new(attributes, &block) else super end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 405f3a30c6..9234029c22 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -90,6 +90,47 @@ module ActiveRecord def logger ActiveRecord::Base.logger end + + def debug(progname = nil, &block) + return unless super + + if ActiveRecord::Base.verbose_query_logs + log_query_source + end + end + + def log_query_source + source_line, line_number = extract_callstack(caller_locations) + + if source_line + if defined?(::Rails.root) + app_root = "#{::Rails.root.to_s}/".freeze + source_line = source_line.sub(app_root, "") + end + + logger.debug(" ↳ #{ source_line }:#{ line_number }") + end + end + + def extract_callstack(callstack) + line = callstack.find do |frame| + frame.absolute_path && !ignored_callstack(frame.absolute_path) + end + + offending_line = line || callstack.first + + [ + offending_line.path, + offending_line.lineno + ] + end + + RAILS_GEM_ROOT = File.expand_path("../../../..", __FILE__) + "/" + + def ignored_callstack(path) + path.start_with?(RAILS_GEM_ROOT) || + path.start_with?(RbConfig::CONFIG["rubylibdir"]) + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d12a979a7f..5322571820 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -550,7 +550,7 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i if @last_check < mtime ActiveRecord::Migration.check_pending!(connection) @last_check = mtime @@ -575,13 +575,14 @@ module ActiveRecord # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) + raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration? end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations? # Roundtrip to Rake to allow plugins to hook into database initialization. - FileUtils.cd Rails.root do + root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root + FileUtils.cd(root) do current_config = Base.connection_config Base.clear_all_connections! system("bin/rails db:test:prepare") @@ -733,6 +734,24 @@ module ActiveRecord execute_block { yield helper } end + # Used to specify an operation that is only run when migrating up + # (for example, populating a new column with its initial values). + # + # In the following example, the new column +published+ will be given + # the value +true+ for all existing records. + # + # class AddPublishedToPosts < ActiveRecord::Migration[5.2] + # def change + # add_column :posts, :published, :boolean, default: false + # up_only do + # execute "update posts set published = 'true'" + # end + # end + # end + def up_only + execute_block { yield } unless reverting? + end + # Runs the given migration classes. # Last argument can specify options: # - :direction (default is :up) @@ -857,10 +876,10 @@ module ActiveRecord FileUtils.mkdir_p(destination) unless File.exist?(destination) - destination_migrations = ActiveRecord::Migrator.migrations(destination) + destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations last = destination_migrations.last sources.each do |scope, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + source_migrations = ActiveRecord::MigrationContext.new(path).migrations source_migrations.each do |migration| source = File.binread(migration.filename) @@ -978,131 +997,147 @@ module ActiveRecord end end - class Migrator#:nodoc: - class << self - attr_writer :migrations_paths - alias :migrations_path= :migrations_paths= - - def migrate(migrations_paths, target_version = nil, &block) - case - when target_version.nil? - up(migrations_paths, target_version, &block) - when current_version == 0 && target_version == 0 - [] - when current_version > target_version - down(migrations_paths, target_version, &block) - else - up(migrations_paths, target_version, &block) - end - end + class MigrationContext # :nodoc: + attr_reader :migrations_paths - def rollback(migrations_paths, steps = 1) - move(:down, migrations_paths, steps) - end + def initialize(migrations_paths) + @migrations_paths = migrations_paths + end - def forward(migrations_paths, steps = 1) - move(:up, migrations_paths, steps) + def migrate(target_version = nil, &block) + case + when target_version.nil? + up(target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(target_version, &block) + else + up(target_version, &block) end + end + + def rollback(steps = 1) + move(:down, steps) + end - def up(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + def forward(steps = 1) + move(:up, steps) + end - new(:up, migrations, target_version).migrate + def up(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def down(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + Migrator.new(:up, selected_migrations, target_version).migrate + end - new(:down, migrations, target_version).migrate + def down(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def run(direction, migrations_paths, target_version) - new(direction, migrations(migrations_paths), target_version).run - end + Migrator.new(:down, selected_migrations, target_version).migrate + end - def open(migrations_paths) - new(:up, migrations(migrations_paths), nil) - end + def run(direction, target_version) + Migrator.new(direction, migrations, target_version).run + end - def get_all_versions(connection = Base.connection) - if SchemaMigration.table_exists? - SchemaMigration.all_versions.map(&:to_i) - else - [] - end - end + def open + Migrator.new(:up, migrations, nil) + end - def current_version(connection = Base.connection) - get_all_versions(connection).max || 0 + def get_all_versions + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end + end - def needs_migration?(connection = Base.connection) - (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 - end + def current_version + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError + end - def any_migrations? - migrations(migrations_paths).any? - end + def needs_migration? + (migrations.collect(&:version) - get_all_versions).size > 0 + end - def last_migration #:nodoc: - migrations(migrations_paths).last || NullMigration.new - end + def any_migrations? + migrations.any? + end - def migrations_paths - @migrations_paths ||= ["db/migrate"] - # just to not break things if someone uses: migrations_path = some_string - Array(@migrations_paths) - end + def last_migration #:nodoc: + migrations.last || NullMigration.new + end - def parse_migration_filename(filename) # :nodoc: - File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + + def migrations + migrations = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + MigrationProxy.new(name, version, file, scope) end - def migrations(paths) - paths = Array(paths) + migrations.sort_by(&:version) + end - migrations = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - name = name.camelize + def migrations_status + db_list = ActiveRecord::SchemaMigration.normalized_versions - MigrationProxy.new(name, version, file, scope) - end + file_list = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact - migrations.sort_by(&:version) + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] end - def migrations_status(paths) - paths = Array(paths) - - db_list = ActiveRecord::SchemaMigration.normalized_versions + (db_list + file_list).sort_by { |_, version, _| version } + end - file_list = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end + def current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end - (db_list + file_list).sort_by { |_, version, _| version } - end + def protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end - def migration_files(paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end + def last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - private + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end - def move(direction, migrations_paths, steps) - migrator = new(direction, migrations(migrations_paths)) + private + def move(direction, steps) + migrator = Migrator.new(direction, migrations) if current_version != 0 && !migrator.current_migration raise UnknownMigrationVersionError.new(current_version) @@ -1117,10 +1152,29 @@ module ActiveRecord finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + send(direction, version) + end + end + + class Migrator # :nodoc: + class << self + attr_accessor :migrations_paths + + def migrations_path=(path) + ActiveSupport::Deprecation.warn \ + "ActiveRecord::Migrator.migrations_paths= 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 + + # For cases where a table doesn't exist like loading from schema cache + def current_version + MigrationContext.new(migrations_paths).current_version end end + self.migrations_paths = ["db/migrate"] + def initialize(direction, migrations, target_version = nil) @direction = direction @target_version = target_version @@ -1183,7 +1237,7 @@ module ActiveRecord end def load_migrated - @migrated_versions = Set.new(self.class.get_all_versions) + @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) end private @@ -1215,7 +1269,7 @@ module ActiveRecord # Stores the current environment in the database. def record_environment return if down? - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end def ran?(migration) @@ -1257,10 +1311,10 @@ module ActiveRecord end def validate(migrations) - name , = migrations.group_by(&:name).find { |_, v| v.length > 1 } + name, = migrations.group_by(&:name).find { |_, v| v.length > 1 } raise DuplicateMigrationNameError.new(name) if name - version , = migrations.group_by(&:version).find { |_, v| v.length > 1 } + version, = migrations.group_by(&:version).find { |_, v| v.length > 1 } raise DuplicateMigrationVersionError.new(version) if version end @@ -1274,23 +1328,6 @@ module ActiveRecord end end - def self.last_stored_environment - return nil if current_version == 0 - raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - - environment = ActiveRecord::InternalMetadata[:environment] - raise NoEnvironmentInSchemaError unless environment - environment - end - - def self.current_environment - ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - end - - def self.protected_environment? - ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment - end - def up? @direction == :up end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index ac7d506fd1..81ef4828f8 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -110,7 +110,7 @@ module ActiveRecord private - module StraightReversions + module StraightReversions # :nodoc: private { transaction: :transaction, execute_block: :execute_block, diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index c979aaf0a0..0edaaa0cf9 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -13,9 +13,32 @@ module ActiveRecord const_get(name) end - V5_2 = Current + V6_0 = Current + + class V5_2 < V6_0 + end class V5_1 < V5_2 + def change_column(table_name, column_name, type, options = {}) + if adapter_name == "PostgreSQL" + clear_cache! + sql = connection.send(:change_column_sql, table_name, column_name, type, options) + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}" + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + else + super + end + end + + def create_table(table_name, options = {}) + if adapter_name == "Mysql2" + super(table_name, options: "ENGINE=InnoDB", **options) + else + super + end + end end class V5_0 < V5_1 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 12ee4a4137..b04dc04899 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -87,19 +87,6 @@ module ActiveRecord # Sets the name of the internal metadata table. ## - # :singleton-method: protected_environments - # :call-seq: protected_environments - # - # The array of names of environments where destructive actions should be prohibited. By default, - # the value is <tt>["production"]</tt>. - - ## - # :singleton-method: protected_environments= - # :call-seq: protected_environments=(environments) - # - # Sets an array of names of environments where destructive actions should be prohibited. - - ## # :singleton-method: pluralize_table_names # :call-seq: pluralize_table_names # @@ -122,9 +109,9 @@ module ActiveRecord class_attribute :table_name_suffix, instance_writer: false, default: "" class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" - class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] class_attribute :pluralize_table_names, instance_writer: false, default: true + self.protected_environments = ["production"] self.inheritance_column = "type" self.ignored_columns = [].freeze @@ -238,6 +225,21 @@ module ActiveRecord (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, + # the value is <tt>["production"]</tt>. + def protected_environments + if defined?(@protected_environments) + @protected_environments + else + superclass.protected_environments + end + end + + # Sets an array of names of environments where destructive actions should be prohibited. + def protected_environments=(environments) + @protected_environments = environments.map(&:to_s) + end + # Defines the name of the table column which will store the class name on single-table # inheritance situations. # @@ -323,11 +325,11 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= ActiveModel::AttributeSet::Builder.new(attribute_types, primary_key) do |name| - unless columns_hash.key?(name) - _default_attributes[name].dup - end + unless defined?(@attributes_builder) && @attributes_builder + defaults = _default_attributes.except(*(column_names - [primary_key])) + @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults) end + @attributes_builder end def columns_hash # :nodoc: @@ -359,8 +361,9 @@ module ActiveRecord # it). # # +attr_name+ The name of the attribute to retrieve the type for. Must be - # a string + # a string or a symbol. def type_for_attribute(attr_name, &block) + attr_name = attr_name.to_s if block attribute_types.fetch(attr_name, &block) else @@ -376,6 +379,7 @@ module ActiveRecord end def _default_attributes # :nodoc: + load_schema @default_attributes ||= ActiveModel::AttributeSet.new({}) end @@ -423,7 +427,7 @@ module ActiveRecord # end def reset_column_information connection.clear_cache! - undefine_attribute_methods + ([self] + descendants).each(&:undefine_attribute_methods) connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 435c81c153..fa20bce3a9 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -63,6 +63,18 @@ module ActiveRecord # member.update params[:member] # member.avatar.icon # => 'sad' # + # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option. + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar, update_only: true + # end + # + # params = { member: { avatar_attributes: { icon: 'sad' } } } + # member.update params[:member] + # member.avatar.id # => 2 + # member.avatar.icon # => 'sad' + # # By default you will only be able to set and update attributes on the # associated model. If you want to destroy the associated model through the # attributes hash, you have to enable it first using the diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a57c60ffac..a45d011d75 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -99,7 +99,9 @@ module ActiveRecord # for updating all records in a single query. def update(id = :all, attributes) if id.is_a?(Array) - id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }.compact + id.map { |one_id| find(one_id) }.each_with_index { |object, idx| + object.update(attributes[idx]) + } elsif id == :all all.each { |record| record.update(attributes) } else @@ -112,7 +114,6 @@ module ActiveRecord object.update(attributes) object end - rescue RecordNotFound end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, @@ -136,11 +137,10 @@ module ActiveRecord # Todo.destroy(todos) def destroy(id) if id.is_a?(Array) - id.map { |one_id| destroy(one_id) }.compact + find(id).each(&:destroy) else find(id).destroy end - rescue RecordNotFound end # Deletes the row with a primary key matching the +id+ argument, using a @@ -165,6 +165,38 @@ module ActiveRecord where(primary_key => id_or_array).delete_all end + def _insert_record(values) # :nodoc: + primary_key_value = nil + + if primary_key && Hash === values + arel_primary_key = arel_attribute(primary_key) + primary_key_value = values[arel_primary_key] + + if !primary_key_value && prefetch_primary_key? + primary_key_value = next_sequence_value + values[arel_primary_key] = primary_key_value + end + end + + if values.empty? + im = arel_table.compile_insert(connection.empty_insert_statement_value) + im.into arel_table + else + im = arel_table.compile_insert(_substitute_values(values)) + end + + connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) + end + + def _update_record(values, id, id_was) # :nodoc: + bind = predicate_builder.build_bind_attribute(primary_key, id_was || id) + um = arel_table.where( + arel_attribute(primary_key).eq(bind) + ).compile_update(_substitute_values(values), primary_key) + + connection.update(um, "#{self} Update") + end + private # Called by +instantiate+ to decide which class to use for a new # record instance. @@ -174,6 +206,13 @@ module ActiveRecord def discriminate_class_for_record(record) self end + + def _substitute_values(values) + values.map do |attr, value| + bind = predicate_builder.build_bind_attribute(attr.name, value) + [attr, bind] + end + end end # Returns true if this object hasn't been saved yet -- that is, a record @@ -320,10 +359,10 @@ module ActiveRecord # Any change to the attributes on either instance will affect both instances. # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) - became = klass.new + became = klass.allocate + became.send(:initialize) became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) - became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.errors.copy!(errors) @@ -363,11 +402,7 @@ module ActiveRecord verify_readonly_attribute(name) public_send("#{name}=", value) - if has_changes_to_save? - save(validate: false) - else - true - end + save(validate: false) end # Updates the attributes of the model from the passed-in hash and saves the @@ -659,6 +694,7 @@ module ActiveRecord def create_or_update(*args, &block) _raise_readonly_record_error if readonly? + return false if destroyed? result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end @@ -671,7 +707,7 @@ module ActiveRecord rows_affected = 0 @_trigger_update_callback = true else - rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database + rows_affected = self.class._update_record(attributes_values, id, id_in_database) @_trigger_update_callback = rows_affected > 0 end @@ -685,7 +721,7 @@ module ActiveRecord def _create_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_create(attribute_names) - new_id = self.class.unscoped.insert attributes_values + new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 3d5babb8b7..8e23128333 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -26,16 +26,19 @@ module ActiveRecord end def self.run - caching_pool = ActiveRecord::Base.connection_pool - caching_was_enabled = caching_pool.query_cache_enabled + ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool| + caching_was_enabled = pool.query_cache_enabled - caching_pool.enable_query_cache! + pool.enable_query_cache! - [caching_pool, caching_was_enabled] + [pool, caching_was_enabled] + end end - def self.complete((caching_pool, caching_was_enabled)) - caching_pool.disable_query_cache! unless caching_was_enabled + def self.complete(caching_pools) + caching_pools.each do |pool, caching_was_enabled| + pool.disable_query_cache! unless caching_was_enabled + end ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 812e1d7a00..ade5946dd5 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -59,6 +59,7 @@ module ActiveRecord console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + ActiveRecord::Base.verbose_query_logs = false end runner do @@ -90,12 +91,16 @@ module ActiveRecord filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) + current_version = ActiveRecord::Migrator.current_version + + next if current_version.nil? + cache = YAML.load(File.read(filename)) - if cache.version == ActiveRecord::Migrator.current_version + if cache.version == current_version connection.schema_cache = cache connection_pool.schema_cache = cache.dup else - warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." + warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}." end end end @@ -177,7 +182,16 @@ end_warning initializer "active_record.clear_active_connections" do config.after_initialize do ActiveSupport.on_load(:active_record) do + # Ideally the application doesn't connect to the database during boot, + # but sometimes it does. In case it did, we want to empty out the + # connection pools so that a non-database-using process (e.g. a master + # process in a forking server model) doesn't retain a needless + # connection. If it was needed, the incremental cost of reestablishing + # this connection is trivial: the rest of the pool would need to be + # populated anyway. + clear_active_connections! + flush_idle_connections! end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 3bca2982e0..2e55713311 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -4,16 +4,16 @@ require "active_record" db_namespace = namespace :db do desc "Set the environment value for the database" - task "environment:set" => [:environment, :load_config] do + task "environment:set" => :load_config do ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end - task check_protected_environments: [:environment, :load_config] do + task check_protected_environments: :load_config do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end - task :load_config do + task load_config: :environment do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end @@ -56,7 +56,7 @@ db_namespace = namespace :db do end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." - task migrate: [:environment, :load_config] do + task migrate: :load_config do ActiveRecord::Tasks::DatabaseTasks.migrate db_namespace["_dump"].invoke end @@ -78,7 +78,7 @@ db_namespace = namespace :db do namespace :migrate do # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' - task redo: [:environment, :load_config] do + task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? if ENV["VERSION"] @@ -94,35 +94,33 @@ db_namespace = namespace :db do task reset: ["db:drop", "db:create", "db:migrate"] # desc 'Runs the "up" for a given migration VERSION.' - task up: [:environment, :load_config] do + task up: :load_config do raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :up, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke end # desc 'Runs the "down" for a given migration VERSION.' - task down: [:environment, :load_config] do + task down: :load_config do raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :down, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke end desc "Display status of migrations" - task status: [:environment, :load_config] do + task status: :load_config do unless ActiveRecord::SchemaMigration.table_exists? abort "Schema migrations table does not exist yet." end @@ -131,8 +129,7 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths - ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -140,16 +137,16 @@ db_namespace = namespace :db do end desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." - task rollback: [:environment, :load_config] do + task rollback: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.rollback(step) db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' - task forward: [:environment, :load_config] do + task forward: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.forward(step) db_namespace["_dump"].invoke end @@ -157,12 +154,12 @@ db_namespace = namespace :db do task reset: [ "db:drop", "db:setup" ] # desc "Retrieves the charset for the current environment's database" - task charset: [:environment, :load_config] do + task charset: :load_config do puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task collation: [:environment, :load_config] do + task collation: :load_config do begin puts ActiveRecord::Tasks::DatabaseTasks.collation_current rescue NoMethodError @@ -171,13 +168,13 @@ db_namespace = namespace :db do end desc "Retrieves the current schema version number" - task version: [:environment, :load_config] do - puts "Current version: #{ActiveRecord::Migrator.current_version}" + task version: :load_config do + puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}" end # desc "Raises an error if there are pending migrations" - task abort_if_pending_migrations: [:environment, :load_config] do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations + task abort_if_pending_migrations: :load_config do + pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -199,7 +196,7 @@ db_namespace = namespace :db do namespace :fixtures do desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task load: [:environment, :load_config] do + task load: :load_config do require "active_record/fixtures" base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -221,7 +218,7 @@ db_namespace = namespace :db do end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task identify: [:environment, :load_config] do + task identify: :load_config do require "active_record/fixtures" label, id = ENV["LABEL"], ENV["ID"] @@ -247,7 +244,7 @@ db_namespace = namespace :db do namespace :schema do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" - task dump: [:environment, :load_config] do + task dump: :load_config do require "active_record/schema_dumper" filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") File.open(filename, "w:utf-8") do |file| @@ -257,7 +254,7 @@ db_namespace = namespace :db do end desc "Loads a schema.rb file into the database" - task load: [:environment, :load_config, :check_protected_environments] do + task load: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"]) end @@ -267,14 +264,14 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." - task dump: [:environment, :load_config] do + 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) end desc "Clears a db/schema_cache.yml file." - task clear: [:environment, :load_config] do + task clear: :load_config do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") rm_f filename, verbose: false end @@ -284,7 +281,7 @@ 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: [:environment, :load_config] do + task dump: :load_config do filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) @@ -299,7 +296,7 @@ db_namespace = namespace :db do end desc "Recreates the databases from the structure.sql file" - task load: [:environment, :load_config, :check_protected_environments] do + task load: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"]) end @@ -338,12 +335,12 @@ db_namespace = namespace :db do end # desc "Empty the test database" - task purge: %w(environment load_config check_protected_environments) do + task purge: %w(load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] end # desc 'Load the test schema' - task prepare: %w(environment load_config) do + task prepare: :load_config do unless ActiveRecord::Base.configurations.blank? db_namespace["test:load"].invoke end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 87bfd75bca..e640d75d2f 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -34,7 +34,8 @@ module ActiveRecord def self.add_reflection(ar, name, reflection) ar.clear_reflections_cache - ar._reflections = ar._reflections.merge(name.to_s => reflection) + name = name.to_s + ar._reflections = ar._reflections.except(name).merge!(name => reflection) end def self.add_aggregate_reflection(ar, name, reflection) @@ -290,10 +291,14 @@ module ActiveRecord end def build_scope(table, predicate_builder = predicate_builder(table)) - Relation.create(klass, table, predicate_builder) + Relation.create( + klass, + table: table, + predicate_builder: predicate_builder + ) end - def join_primary_key(_) + def join_primary_key(*) foreign_key end @@ -458,10 +463,6 @@ module ActiveRecord options[:primary_key] || primary_key(klass || self.klass) end - def association_primary_key_type - klass.type_for_attribute(association_primary_key.to_s) - end - def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end @@ -567,7 +568,7 @@ module ActiveRecord end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] - INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key] def add_as_source(seed) seed @@ -722,7 +723,7 @@ module ActiveRecord end end - def join_primary_key(klass) + def join_primary_key(klass = nil) polymorphic? ? association_primary_key(klass) : association_primary_key end @@ -859,10 +860,6 @@ module ActiveRecord actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass) end - def association_primary_key_type - klass.type_for_attribute(association_primary_key.to_s) - end - # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. # # class Post < ActiveRecord::Base diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e2d2f45503..d71c430045 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,7 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :skip_query_cache] CLAUSE_METHODS = [:where, :having, :from] - INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] + INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS @@ -22,7 +22,7 @@ module ActiveRecord alias :loaded? :loaded alias :locked? :lock_value - def initialize(klass, table, predicate_builder, values = {}) + def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {}) @klass = klass @table = table @values = values @@ -36,67 +36,6 @@ module ActiveRecord reset end - def insert(values) # :nodoc: - primary_key_value = nil - - if primary_key && Hash === values - primary_key_value = values[values.keys.find { |k| - k.name == primary_key - }] - - if !primary_key_value && klass.prefetch_primary_key? - primary_key_value = klass.next_sequence_value - values[arel_attribute(klass.primary_key)] = primary_key_value - end - end - - im = arel.create_insert - im.into @table - - substitutes = substitute_values values - - if values.empty? # empty insert - im.values = Arel.sql(connection.empty_insert_statement_value) - else - im.insert substitutes - end - - @klass.connection.insert( - im, - "#{@klass} Create", - primary_key || false, - primary_key_value, - nil, - ) - end - - def _update_record(values, id, id_was) # :nodoc: - substitutes = substitute_values values - - scope = @klass.unscoped - - if @klass.finder_needs_type_condition? - scope.unscope!(where: @klass.inheritance_column) - end - - relation = scope.where(@klass.primary_key => (id_was || id)) - um = relation - .arel - .compile_update(substitutes, @klass.primary_key) - - @klass.connection.update( - um, - "#{@klass} Update", - ) - end - - def substitute_values(values) # :nodoc: - values.map do |arel_attr, value| - bind = predicate_builder.build_bind_attribute(arel_attr.name, value) - [arel_attr, bind] - end - end - def arel_attribute(name) # :nodoc: klass.arel_attribute(name, table) end @@ -113,8 +52,8 @@ module ActiveRecord # # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar - def new(*args, &block) - scoping { @klass.new(*args, &block) } + def new(attributes = nil, &block) + scoping { klass.new(scope_for_create(attributes), &block) } end alias build new @@ -138,8 +77,12 @@ module ActiveRecord # # users.create(name: nil) # validation on name # # => #<User id: nil, name: nil, ...> - def create(*args, &block) - scoping { @klass.create(*args, &block) } + 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 end # Similar to #create, but calls @@ -148,8 +91,12 @@ module ActiveRecord # # Expects arguments in the same format as # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. - def create!(*args, &block) - scoping { @klass.create!(*args, &block) } + 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 end def first_or_create(attributes = nil, &block) # :nodoc: @@ -367,10 +314,10 @@ module ActiveRecord stmt = Arel::UpdateManager.new - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates)) stmt.table(table) - if has_join_values? + if has_join_values? || offset_value @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else stmt.key = arel_attribute(primary_key) @@ -418,8 +365,8 @@ module ActiveRecord # # If an invalid method is supplied, #delete_all raises an ActiveRecordError: # - # Post.limit(100).delete_all - # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit + # Post.distinct.delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct def delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| value = get_value(method) @@ -437,7 +384,7 @@ module ActiveRecord stmt = Arel::DeleteManager.new stmt.from(table) - if has_join_values? + if has_join_values? || has_limit_or_offset? @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) else stmt.wheres = arel.constraints @@ -483,7 +430,7 @@ module ActiveRecord relation = self if eager_loading? - find_with_associations { |rel, _| relation = rel } + apply_join_dependency { |rel, _| relation = rel } end conn = klass.connection @@ -501,8 +448,10 @@ module ActiveRecord where_clause.to_h(relation_table_name) end - def scope_for_create - where_values_hash.merge!(create_with_value.stringify_keys) + def scope_for_create(attributes = nil) + scope = where_values_hash.merge!(create_with_value.stringify_keys) + scope.merge!(attributes) if attributes + scope end # Returns true if relation needs eager loading. @@ -584,7 +533,7 @@ module ActiveRecord skip_query_cache_if_necessary do @records = if eager_loading? - find_with_associations do |relation, join_dependency| + apply_join_dependency do |relation, join_dependency| if ActiveRecord::NullRelation === relation [] else diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 11256ab3d9..6a63eb70b1 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -131,7 +131,14 @@ module ActiveRecord def calculate(operation, column_name) if has_include?(column_name) relation = apply_join_dependency - relation.distinct! if operation.to_s.downcase == "count" + + if operation.to_s.downcase == "count" && !distinct_value + relation.distinct! + # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT + if (column_name == :all || column_name.nil?) && select_values.empty? + relation.order_values = [] + end + end relation.calculate(operation, column_name) else @@ -183,6 +190,7 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else + enforce_raw_sql_whitelist(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn @@ -192,6 +200,24 @@ module ActiveRecord end end + # Pick the value(s) from the named column(s) in the current relation. + # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful + # when you have a relation that's already narrowed down to a single row. + # + # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also + # more efficient. The value is, again like with pluck, typecast by the column type. + # + # Person.where(id: 1).pick(:name) + # # SELECT people.name FROM people WHERE id = 1 LIMIT 1 + # # => 'David' + # + # Person.where(id: 1).pick(:name, :email_address) + # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1 + # # => [ 'David', 'david@loudthinking.com' ] + def pick(*column_names) + limit(1).pluck(*column_names).first + end + # Pluck all the ID's for the relation using the table's primary key # # Person.ids # SELECT people.id FROM people @@ -216,7 +242,7 @@ module ActiveRecord if operation == "count" column_name ||= select_for_count if column_name == :all - if distinct && (group_values.any? || !(has_limit_or_offset? && order_values.any?)) + if distinct && (group_values.any? || select_values.empty? && order_values.empty?) column_name = primary_key end elsif column_name =~ /\s*DISTINCT[\s(]+/i @@ -234,7 +260,7 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) @@ -248,7 +274,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column_alias = column_name - if operation == "count" && has_limit_or_offset? + if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?) # Shortcut when limit is zero. return 0 if limit_value == 0 @@ -390,14 +416,12 @@ module ActiveRecord end def build_count_subquery(relation, column_name, distinct) - relation.select_values = [ - if column_name == :all - distinct ? table[Arel.star] : Arel.sql(FinderMethods::ONE_AS_ONE) - else - column_alias = Arel.sql("count_column") - aggregate_column(column_name).as(column_alias) - end - ] + if column_name == :all + relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct + else + column_alias = Arel.sql("count_column") + relation.select_values = [ aggregate_column(column_name).as(column_alias) ] + end subquery = relation.arel.as(Arel.sql("subquery_for_count")) select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 18566b5662..5f959af5dc 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -18,9 +18,10 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order - # option if you want the results to be sorted. + # NOTE: The returned records are in the same order as the ids you provide. + # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where + # method and provide an explicit ActiveRecord::QueryMethods#order option. + # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound. # # ==== Find with lock # @@ -88,7 +89,7 @@ module ActiveRecord where(arg, *args).take! rescue ::RangeError raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", - @klass.name) + @klass.name, @klass.primary_key) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -147,7 +148,7 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - return find_last(limit) if loaded? || limit_value + return find_last(limit) if loaded? || has_limit_or_offset? result = ordered_relation.limit(limit) result = result.reverse_order! @@ -284,7 +285,7 @@ module ActiveRecord # * Hash - Finds the record that matches these +find+-style conditions # (such as <tt>{name: 'David'}</tt>). # * +false+ - Returns always +false+. - # * No args - Returns +false+ if the table is empty, +true+ otherwise. + # * No args - Returns +false+ if the relation is empty, +true+ otherwise. # # For more information about specifying conditions as a hash or array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -300,6 +301,7 @@ module ActiveRecord # Person.exists?(name: 'David') # Person.exists?(false) # Person.exists? + # Person.where(name: 'Spartacus', rating: 4).exists? def exists?(conditions = :none) if Base === conditions raise ArgumentError, <<-MSG.squish @@ -311,7 +313,7 @@ module ActiveRecord return false if !conditions || limit_value == 0 if eager_loading? - relation = apply_join_dependency(construct_join_dependency(eager_loading: false)) + relation = apply_join_dependency(eager_loading: false) return relation.exists?(conditions) end @@ -338,7 +340,7 @@ module ActiveRecord if ids.nil? error = "Couldn't find #{name}".dup error << " with#{conditions}" if conditions - raise RecordNotFound.new(error, name) + 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) @@ -346,7 +348,7 @@ module ActiveRecord error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup 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, primary_key, ids) + raise RecordNotFound.new(error, name, key, ids) end end @@ -356,24 +358,6 @@ module ActiveRecord offset_value || 0 end - def find_with_associations - # NOTE: the JoinDependency constructed here needs to know about - # any joins already present in `self`, so pass them in - # - # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 - # incorrect SQL is generated. In that case, the join dependency for - # SpecialCategorizations is constructed without knowledge of the - # preexisting join in joins_values to categorizations (by way of - # the `has_many :through` for categories). - # - join_dependency = construct_join_dependency - - relation = apply_join_dependency(join_dependency) - relation._select!(join_dependency.aliases.columns) - - yield relation, join_dependency - end - def construct_relation_for_exists(conditions) relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) @@ -389,22 +373,29 @@ module ActiveRecord def construct_join_dependency(eager_loading: true) including = eager_load_values + includes_values + joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) } ActiveRecord::Associations::JoinDependency.new( - klass, table, including, alias_tracker(joins_values), eager_loading: eager_loading + klass, table, including, alias_tracker(joins), eager_loading: eager_loading ) end - def apply_join_dependency(join_dependency = construct_join_dependency) + def apply_join_dependency(eager_loading: true) + join_dependency = construct_join_dependency(eager_loading: eager_loading) relation = except(:includes, :eager_load, :preload).joins!(join_dependency) - if using_limitable_reflections?(join_dependency.reflections) - relation - else - if relation.limit_value + if eager_loading && !using_limitable_reflections?(join_dependency.reflections) + if has_limit_or_offset? limited_ids = limited_ids_for(relation) limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end - relation.except(:limit, :offset) + relation.limit_value = relation.offset_value = nil + end + + if block_given? + relation._select!(join_dependency.aliases.columns) + yield relation, join_dependency + else + relation end end @@ -432,9 +423,12 @@ module ActiveRecord ids = ids.flatten.compact.uniq + model_name = @klass.name + case ids.size when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + error_message = "Couldn't find #{model_name} without an ID" + raise RecordNotFound.new(error_message, model_name, primary_key) when 1 result = find_one(ids.first) expects_array ? [ result ] : result @@ -442,7 +436,8 @@ module ActiveRecord find_some(ids) end rescue ::RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + error_message = "Couldn't find #{model_name} with an out of range ID" + raise RecordNotFound.new(error_message, model_name, primary_key, ids) end def find_one(id) @@ -526,7 +521,11 @@ module ActiveRecord else relation = ordered_relation - if limit_value.nil? || index < limit_value + if limit_value + limit = [limit_value - index, limit].min + end + + if limit > 0 relation = relation.offset(offset_index + index) unless index.zero? relation.limit(limit).to_a else @@ -541,12 +540,11 @@ module ActiveRecord else relation = ordered_relation - relation.to_a[-index] - # TODO: can be made more performant on large result sets by - # for instance, last(index)[-index] (which would require - # refactoring the last(n) finder method to make test suite pass), - # or by using a combination of reverse_order, limit, and offset, - # e.g., reverse_order.offset(index-1).first + if equal?(relation) || has_limit_or_offset? + relation.records[-index] + else + relation.last(index)[-index] + end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index b736b21525..25510d4a57 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -23,7 +23,11 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table, relation.predicate_builder) + other = Relation.create( + relation.klass, + table: relation.table, + predicate_builder: relation.predicate_builder + ) hash.each { |k, v| if k == :joins if Hash === v @@ -52,7 +56,7 @@ module ActiveRecord NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - - [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: + [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -79,6 +83,7 @@ module ActiveRecord merge_clauses merge_preloads merge_joins + merge_outer_joins relation end @@ -129,6 +134,29 @@ module ActiveRecord end end + def merge_outer_joins + return if other.left_outer_joins_values.blank? + + if other.klass == relation.klass + relation.left_outer_joins!(*other.left_outer_joins_values) + else + alias_tracker = nil + joins_dependency = other.left_outer_joins_values.map do |join| + case join + when Hash, Symbol, Array + alias_tracker ||= other.alias_tracker + ActiveRecord::Associations::JoinDependency.new( + other.klass, other.table, join, alias_tracker + ) + else + join + end + end + + relation.left_outer_joins!(*joins_dependency) + end + end + def merge_multi_values if other.reordering_value # override any order specified in the original relation diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 885c26d7aa..f3286846d2 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -86,6 +86,18 @@ module ActiveRecord expand_from_hash(query).reduce(&:and) end queries.reduce(&:or) + elsif table.aggregated_with?(key) + mapping = table.reflect_on_aggregation(key).mapping + queries = Array.wrap(value).map do |object| + mapping.map do |field_attr, aggregate_attr| + if mapping.size == 1 && !object.respond_to?(aggregate_attr) + build(table.arel_attribute(field_attr), object) + else + build(table.arel_attribute(field_attr), object.send(aggregate_attr)) + end + end.reduce(&:and) + end + queries.reduce(&:or) # FIXME: Deprecate this and provide a public API to force equality elsif (value.is_a?(Range) || value.is_a?(Array)) && table.type(key.to_s).respond_to?(:subtype) diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb index 0255a65bfe..28c7483c95 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -30,7 +30,7 @@ module ActiveRecord end def primary_key - associated_table.association_join_keys.key + associated_table.association_join_primary_key end def convert_to_id(value) diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb index b87b5c36dd..e8e2f2c626 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -29,7 +29,7 @@ module ActiveRecord end def primary_key(value) - associated_table.association_primary_key(base_class(value)) + associated_table.association_join_primary_key(base_class(value)) end def base_class(value) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index eec907e9cf..3afa368575 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -296,6 +296,7 @@ module ActiveRecord spawn.order!(*args) end + # Same as #order but operates on relation in-place instead of copying. def order!(*args) # :nodoc: preprocess_order_args(args) @@ -317,6 +318,7 @@ module ActiveRecord spawn.reorder!(*args) end + # Same as #reorder but operates on relation in-place instead of copying. def reorder!(*args) # :nodoc: preprocess_order_args(args) @@ -326,8 +328,8 @@ module ActiveRecord end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, - :limit, :offset, :joins, :includes, :from, - :readonly, :having]) + :limit, :offset, :joins, :left_outer_joins, + :includes, :from, :readonly, :having]) # Removes an unwanted relation that is already defined on a chain of relations. # This is useful when passing around chains of relations and would like to @@ -374,10 +376,11 @@ module ActiveRecord args.each do |scope| case scope when Symbol + scope = :left_outer_joins if scope == :left_joins if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - set_value(scope, nil) + set_value(scope, DEFAULT_VALUES[scope]) when Hash scope.each do |key, target_value| if key != :where @@ -443,15 +446,13 @@ module ActiveRecord # def left_outer_joins(*args) check_if_method_has_arguments!(__callee__, args) - - args.compact! - args.flatten! - spawn.left_outer_joins!(*args) end alias :left_joins :left_outer_joins def left_outer_joins!(*args) # :nodoc: + args.compact! + args.flatten! self.left_outer_joins_values += args self end @@ -906,7 +907,7 @@ module ActiveRecord protected # Returns a relation value with a given name def get_value(name) # :nodoc: - @values[name] || default_value_for(name) + @values.fetch(name, DEFAULT_VALUES[name]) end # Sets the relation value with the given name @@ -979,6 +980,8 @@ module ActiveRecord case join when Hash, Symbol, Array :association_join + when ActiveRecord::Associations::JoinDependency + :stashed_join else raise ArgumentError, "only Hash, Symbol and Array are allowed" end @@ -1014,7 +1017,7 @@ module ActiveRecord join_nodes = buckets[:join_node].uniq string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(string_joins) alias_tracker = alias_tracker(join_list, aliases) join_dependency = ActiveRecord::Associations::JoinDependency.new( @@ -1029,7 +1032,7 @@ module ActiveRecord alias_tracker.aliases end - def convert_join_strings_to_ast(table, joins) + def convert_join_strings_to_ast(joins) joins .flatten .reject(&:blank?) @@ -1039,6 +1042,8 @@ module ActiveRecord def build_select(arel) if select_values.any? arel.project(*arel_columns(select_values.uniq)) + elsif klass.ignored_columns.any? + arel.project(*klass.column_names.map { |field| arel_attribute(field) }) else arel.project(table[Arel.star]) end @@ -1075,7 +1080,7 @@ module ActiveRecord end o.split(",").map! do |s| s.strip! - s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC") + s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC") end else o @@ -1084,6 +1089,10 @@ module ActiveRecord end def does_not_support_reverse?(order) + # Account for String subclasses like Arel::Nodes::SqlLiteral that + # override methods like #count. + order = String.new(order) unless order.instance_of?(String) + # Uses SQL function with multiple arguments. (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || # Uses "nulls first" like construction. @@ -1114,9 +1123,15 @@ module ActiveRecord def preprocess_order_args(order_args) order_args.map! do |arg| - klass.send(:sanitize_sql_for_order, arg) + klass.sanitize_sql_for_order(arg) end order_args.flatten! + + @klass.enforce_raw_sql_whitelist( + order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, + whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + ) + validate_order_args(order_args) references = order_args.grep(String) @@ -1179,7 +1194,6 @@ module ActiveRecord DEFAULT_VALUES = { create_with: FROZEN_EMPTY_HASH, - readonly: false, where: Relation::WhereClause.empty, having: Relation::WhereClause.empty, from: Relation::FromClause.empty @@ -1188,15 +1202,5 @@ module ActiveRecord Relation::MULTI_VALUE_METHODS.each do |value| DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY end - - Relation::SINGLE_VALUE_METHODS.each do |value| - DEFAULT_VALUES[value] = nil if DEFAULT_VALUES[value].nil? - end - - def default_value_for(name) - DEFAULT_VALUES.fetch(name) do - raise ArgumentError, "unknown relation value #{name.inspect}" - end - end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 617d8de8b2..562e04194c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -69,7 +69,7 @@ module ActiveRecord private def relation_with(values) - result = Relation.create(klass, table, predicate_builder, values) + result = Relation.create(klass, values: values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 752bb38481..a502713e56 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -47,7 +47,7 @@ module ActiveRecord end def to_h(table_name = nil) - equalities = predicates.grep(Arel::Nodes::Equality) + equalities = equalities(predicates) if table_name equalities = equalities.select do |node| node.left.relation.name == table_name @@ -90,6 +90,20 @@ module ActiveRecord end private + def equalities(predicates) + equalities = [] + + predicates.each do |node| + case node + when Arel::Nodes::Equality + equalities << node + when Arel::Nodes::And + equalities.concat equalities(node.children) + end + end + + equalities + end def predicates_unreferenced_by(other) predicates.reject do |n| @@ -121,7 +135,7 @@ module ActiveRecord end def except_predicates(columns) - self.predicates.reject do |node| + predicates.reject do |node| case node when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 1374785354..92b720068c 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -11,10 +11,9 @@ module ActiveRecord def build(opts, other) case opts when String, Array - parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))] when Hash attributes = predicate_builder.resolve_column_aliases(opts) - attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! parts = predicate_builder.build_from_hash(attributes) diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1c3099f55c..c6c268855e 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -5,70 +5,135 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - private + # Accepts an array or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # + # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition) + return nil if condition.blank? - # Accepts an array or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a WHERE clause. - # - # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) - # # => "name='foo''bar' and group_id='4'" - # - # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) - # # => "name='foo''bar' and group_id='4'" - # - # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") - # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition) # :doc: - return nil if condition.blank? - - case condition - when Array; sanitize_sql_array(condition) - else condition - end + case condition + when Array; sanitize_sql_array(condition) + else condition end - alias :sanitize_sql :sanitize_sql_for_conditions + end + alias :sanitize_sql :sanitize_sql_for_conditions - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a SET clause. - # - # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) - # # => "name=NULL and group_id=4" - # - # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) - # # => "name=NULL and group_id=4" - # - # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) - # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" - # - # sanitize_sql_for_assignment("name=NULL and group_id='4'") - # # => "name=NULL and group_id='4'" - def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc: - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) - else assignments - end + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # + # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) + # # => "name=NULL and group_id=4" + # + # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) + # # => "name=NULL and group_id=4" + # + # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 }) + # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" + # + # sanitize_sql_for_assignment("name=NULL and group_id='4'") + # # => "name=NULL and group_id='4'" + def sanitize_sql_for_assignment(assignments, default_table_name = table_name) + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) + else assignments end + end - # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for an ORDER clause. - # - # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) - # # => "field(id, 1,3,2)" - # - # sanitize_sql_for_order("id ASC") - # # => "id ASC" - def sanitize_sql_for_order(condition) # :doc: - if condition.is_a?(Array) && condition.first.to_s.include?("?") - sanitize_sql_array(condition) - else - condition + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for an ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "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 + ) + + # Ensure we aren't dealing with a subclass of String that might + # override methods we use (eg. Arel::Nodes::SqlLiteral). + if condition.first.kind_of?(String) && !condition.first.instance_of?(String) + condition = [String.new(condition.first), *condition[1..-1]] end + + Arel.sql(sanitize_sql_array(condition)) + else + condition + end + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # + # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") + # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" + def sanitize_sql_hash_for_assignment(attrs, table) + c = connection + attrs.map do |attr, value| + type = type_for_attribute(attr) + value = type.serialize(type.cast(value)) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" + end.join(", ") + end + + # Sanitizes a +string+ so that it is safe to use within an SQL + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". + # + # sanitize_sql_like("100%") + # # => "100\\%" + # + # sanitize_sql_like("snake_cased_string") + # # => "snake\\_cased\\_string" + # + # sanitize_sql_like("100%", "!") + # # => "100!%" + # + # sanitize_sql_like("snake_cased_string", "!") + # # => "snake!_cased!_string" + def sanitize_sql_like(string, escape_character = "\\") + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].join } + end + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # + # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_array(ary) + statement, *values = ary + if values.first.is_a?(Hash) && /:\w+/.match?(statement) + replace_named_bind_variables(statement, values.first) + elsif statement.include?("?") + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } end + end + private # Accepts a hash of SQL conditions and replaces those attributes # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] # relationship with their expanded aggregate attribute values. @@ -90,10 +155,12 @@ module ActiveRecord if aggregation = reflect_on_aggregation(attr.to_sym) mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value + expanded_attrs[field_attr] = if value.is_a?(Array) + value.map { |it| it.send(aggregate_attr) } + elsif mapping.size == 1 && !value.respond_to?(aggregate_attr) + value else - expanded_attrs[field_attr] = value.send(aggregate_attr) + value.send(aggregate_attr) end end else @@ -102,61 +169,7 @@ module ActiveRecord end expanded_attrs end - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # - # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") - # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" - def sanitize_sql_hash_for_assignment(attrs, table) # :doc: - c = connection - attrs.map do |attr, value| - value = type_for_attribute(attr.to_s).serialize(value) - "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" - end.join(", ") - end - - # Sanitizes a +string+ so that it is safe to use within an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". - # - # sanitize_sql_like("100%") - # # => "100\\%" - # - # sanitize_sql_like("snake_cased_string") - # # => "snake\\_cased\\_string" - # - # sanitize_sql_like("100%", "!") - # # => "100!%" - # - # sanitize_sql_like("snake_cased_string", "!") - # # => "snake!_cased!_string" - def sanitize_sql_like(string, escape_character = "\\") # :doc: - pattern = Regexp.union(escape_character, "%", "_") - string.gsub(pattern) { |x| [escape_character, x].join } - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # - # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) - # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) # :doc: - statement, *values = ary - if values.first.is_a?(Hash) && /:\w+/.match?(statement) - replace_named_bind_variables(statement, values.first) - elsif statement.include?("?") - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } - end - end + deprecate :expand_hash_conditions_for_aggregates def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 1e121f2a09..216359867c 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -55,7 +55,7 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 66f7d29886..b8d848b999 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -44,7 +44,7 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @version = Migrator::current_version rescue nil + @version = connection.migration_context.current_version rescue nil @options = options end @@ -184,8 +184,9 @@ HEADER "name: #{index.name.inspect}", ] index_parts << "unique: true" if index.unique - index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? - index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? + index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present? + index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present? + index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present? index_parts << "where: #{index.where.inspect}" if index.where index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type @@ -231,6 +232,14 @@ HEADER options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") end + def format_index_parts(options) + if options.is_a?(Hash) + "{ #{format_options(options)} }" + else + options.inspect + end + end + def remove_prefix_and_suffix(table) prefix = Regexp.escape(@options[:table_name_prefix].to_s) suffix = Regexp.escape(@options[:table_name_suffix].to_s) diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index da585a9562..01ac56570a 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,23 +11,23 @@ module ActiveRecord include Named end - module ClassMethods - def current_scope(skip_inherited_scope = false) # :nodoc: + module ClassMethods # :nodoc: + def current_scope(skip_inherited_scope = false) ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) end - def current_scope=(scope) #:nodoc: + 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 # :nodoc: + def scope_attributes all.scope_for_create end # Are there attributes associated with this scope? - def scope_attributes? # :nodoc: + def scope_attributes? current_scope end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 86ae374318..8c612df27a 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -111,7 +111,7 @@ module ActiveRecord # The user has defined their own default scope method, so call that evaluate_default_scope do if scope = default_scope - (base_rel ||= relation).merge(scope) + (base_rel ||= relation).merge!(scope) end end elsif default_scopes.any? @@ -119,7 +119,7 @@ module ActiveRecord evaluate_default_scope do default_scopes.inject(base_rel) do |default_scope, scope| scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) - default_scope.merge(base_rel.instance_exec(&scope)) + default_scope.merge!(base_rel.instance_exec(&scope)) end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 310af72c41..752655aa05 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -171,6 +171,12 @@ module ActiveRecord "a class method with the same name." end + if method_defined_within?(name, Relation) + raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ + "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \ + "an instance method with the same name." + end + valid_scope_name?(name) extension = Module.new(&block) if block diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 202b82fa61..6dbc977f9a 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -135,7 +135,7 @@ module ActiveRecord end def store_accessor_for(store_attribute) - type_for_attribute(store_attribute.to_s).accessor + type_for_attribute(store_attribute).accessor end class HashAccessor # :nodoc: diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index a5187efc84..d3b9171159 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -2,8 +2,7 @@ module ActiveRecord class TableMetadata # :nodoc: - delegate :foreign_type, :foreign_key, :join_keys, :join_foreign_key, to: :association, prefix: true - delegate :association_primary_key, to: :association + delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true def initialize(klass, arel_table, association = nil) @klass = klass @@ -31,7 +30,7 @@ module ActiveRecord def type(column_name) if klass - klass.type_for_attribute(column_name.to_s) + klass.type_for_attribute(column_name) else Type.default_value end @@ -66,6 +65,14 @@ module ActiveRecord association && association.polymorphic? end + def aggregated_with?(aggregation_name) + klass && reflect_on_aggregation(aggregation_name) + end + + def reflect_on_aggregation(aggregation_name) + klass.reflect_on_aggregation(aggregation_name) + end + # TODO Change this to private once we've dropped Ruby 2.2 support. # Workaround for Ruby 2.2 "private attribute?" warning. protected diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 4657e51e6d..d8e0cd1e30 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -54,10 +54,10 @@ module ActiveRecord def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] - current = ActiveRecord::Migrator.current_environment - stored = ActiveRecord::Migrator.last_stored_environment + current = ActiveRecord::Base.connection.migration_context.current_environment + stored = ActiveRecord::Base.connection.migration_context.last_stored_environment - if ActiveRecord::Migrator.protected_environment? + if ActiveRecord::Base.connection.migration_context.protected_environment? raise ActiveRecord::ProtectedEnvironmentError.new(stored) end @@ -117,9 +117,9 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).create - $stdout.puts "Created database '#{configuration['database']}'" + $stdout.puts "Created database '#{configuration['database']}'" if verbose? rescue DatabaseAlreadyExists - $stderr.puts "Database '#{configuration['database']}' already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -144,7 +144,7 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).drop - $stdout.puts "Dropped database '#{configuration['database']}'" + $stdout.puts "Dropped database '#{configuration['database']}'" if verbose? rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error @@ -163,13 +163,17 @@ module ActiveRecord } end + def verbose? + ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + end + def migrate check_target_version - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + verbose = verbose? scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(migrations_paths, target_version) do |migration| + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end ActiveRecord::Base.clear_cache! diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index d51350ba83..c5cfdba8b1 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -9,7 +9,7 @@ module ActiveRecord def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) - type = types.type_for_attribute(attr_name.to_s) + type = types.type_for_attribute(attr_name) type.serialize(value) end diff --git a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt index 60050e0bf8..60050e0bf8 100644 --- a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb +++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt index 5f7201cfe1..5f7201cfe1 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt index 481c70201b..481c70201b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt index 55dc65c8ad..55dc65c8ad 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt index a3bf1c37b6..a3bf1c37b6 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 9aaa2852d0..0092146e09 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -78,7 +78,7 @@ module ActiveRecord idx_name = "accounts_idx" indexes = @connection.indexes("accounts") - assert indexes.empty? + assert_empty indexes @connection.add_index :accounts, :firm_id, name: idx_name indexes = @connection.indexes("accounts") @@ -368,16 +368,16 @@ module ActiveRecord unless in_memory_db? test "transaction state is reset after a reconnect" do @connection.begin_transaction - assert @connection.transaction_open? + assert_predicate @connection, :transaction_open? @connection.reconnect! - assert !@connection.transaction_open? + assert_not_predicate @connection, :transaction_open? end test "transaction state is reset after a disconnect" do @connection.begin_transaction - assert @connection.transaction_open? + assert_predicate @connection, :transaction_open? @connection.disconnect! - assert !@connection.transaction_open? + assert_not_predicate @connection, :transaction_open? 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 4e73c557ed..9ae2c42368 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -68,14 +68,14 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`)) ENGINE=InnoDB" + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`))" actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, type: type end assert_equal expected, actual end - expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10))) ENGINE=InnoDB" + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)))" actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, length: 10, using: :btree end @@ -108,7 +108,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def test_create_mysql_database_with_encoding assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, charset: :big5, collation: :big5_chinese_ci) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end def test_recreate_mysql_database_with_encoding @@ -160,7 +160,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" + expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query" actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| t.index :zip end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index fd5f712f1a..aa870349be 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -14,8 +14,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase end def test_case_sensitive - assert !CollationTest.columns_hash["string_ci_column"].case_sensitive? - assert CollationTest.columns_hash["string_cs_column"].case_sensitive? + assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive? + assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive? end def test_case_insensitive_comparison_for_ci_column diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index e61c70848a..726f58d58e 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -40,29 +40,29 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_no_automatic_reconnection_after_timeout - assert @connection.active? + assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") sleep 2 - assert !@connection.active? + assert_not_predicate @connection, :active? ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect - assert @connection.active? + assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") sleep 2 @connection.reconnect! - assert @connection.active? + assert_predicate @connection, :active? end def test_successful_reconnection_after_timeout_with_verify - assert @connection.active? + assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") sleep 2 @connection.verify! - assert @connection.active? + assert_predicate @connection, :active? end def test_execute_after_disconnect @@ -174,10 +174,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal "SCHEMA", @subscriber.logged[0][1] end - def test_logs_name_rename_column_sql + def test_logs_name_rename_column_for_alter @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" @subscriber.logged.clear - @connection.send(:rename_column_sql, "bar_baz", "foo", "foo2") + @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2") assert_equal "SCHEMA", @subscriber.logged[0][1] ensure @connection.execute "DROP TABLE `bar_baz`" diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 108bec832c..832f5d61d1 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -13,11 +13,11 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase def test_should_not_be_unsigned column = EnumTest.columns_hash["enum_column"] - assert_not column.unsigned? + assert_not_predicate column, :unsigned? end def test_should_not_be_bigint column = EnumTest.columns_hash["enum_column"] - assert_not column.bigint? + assert_not_predicate column, :bigint? end end diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 2736f7cf0e..b8e778f0b0 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true require "cases/helper" -require "models/developer" -require "models/computer" +require "models/author" +require "models/post" class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain + explain = Author.where(id: 1).explain + assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain + assert_match %r(authors |.* const), explain end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain - assert_match %r(audit_logs |.* ALL), explain + explain = Author.where(id: 1).includes(:posts).explain + assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain + assert_match %r(authors |.* const), explain + assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain + assert_match %r(posts |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 62abd694bb..d7d9a2d732 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -36,7 +36,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase assert connection.column_exists?(table_name, :key, :string) end ensure - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb index 9f6bd38a8c..1c92df940f 100644 --- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -42,3 +42,78 @@ class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase assert_match %r{COLLATE=utf8mb4_bin}, options end end + +class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + def setup + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + test "schema dump includes ENGINE=InnoDB if not provided" do + ActiveRecord::Base.connection.create_table "mysql_table_options", force: true + + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=InnoDB}, options + end + + test "schema dump includes ENGINE=InnoDB in legacy migrations" do + migration = Class.new(ActiveRecord::Migration[5.1]) do + def migrate(x) + create_table "mysql_table_options", force: true + end + end.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=InnoDB}, options + end +end + +class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + + def setup + @logger_was = ActiveRecord::Base.logger + @log = StringIO.new + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.logger = @logger_was + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + test "new migrations do not contain default ENGINE=InnoDB option" do + ActiveRecord::Base.connection.create_table "mysql_table_options", force: true + + assert_no_match %r{ENGINE=InnoDB}, @log.string + end + + test "legacy migrations contain default ENGINE=InnoDB option" do + migration = Class.new(ActiveRecord::Migration[5.1]) do + def migrate(x) + create_table "mysql_table_options", force: true + end + end.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_match %r{ENGINE=InnoDB}, @log.string + end +end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index ac9a8d9dfb..f921515c10 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -13,6 +13,7 @@ module ActiveRecord setup do @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) @connection = ActiveRecord::Base.connection @connection.clear_cache! @@ -31,6 +32,7 @@ module ActiveRecord @connection.drop_table "samples", if_exists: true Thread.abort_on_exception = @abort + Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) end test "raises Deadlocked when a deadlock is encountered" do @@ -60,8 +62,8 @@ module ActiveRecord end end - test "raises TransactionTimeout when lock wait timeout exceeded" do - assert_raises(ActiveRecord::TransactionTimeout) do + test "raises LockWaitTimeout when lock wait timeout exceeded" do + assert_raises(ActiveRecord::LockWaitTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new latch2 = Concurrent::CountDownLatch.new @@ -87,5 +89,61 @@ module ActiveRecord end end end + + test "raises StatementTimeout when statement timeout exceeded" do + skip unless ActiveRecord::Base.connection.show_variable("max_execution_time") + assert_raises(ActiveRecord::StatementTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET max_execution_time = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET max_execution_time = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'") + conn.execute("KILL QUERY #{pid}") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end end end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index b01f5d7f5a..97da96003d 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -54,7 +54,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase end @connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column| - assert column.unsigned? + assert_predicate column, :unsigned? end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 9929237546..308ad1d854 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -59,6 +59,15 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true) end + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops)) + assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops }) + + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC)) + assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc }) + + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST)) + assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST") + assert_raise ArgumentError do add_index(:people, :last_name, algorithm: :copy) end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 0e9e86f425..58fa7532a2 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -39,12 +39,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_column assert_equal :string, @column.type assert_equal "character varying(255)", @column.sql_type - assert @column.array? - assert_not @type.binary? + assert_predicate @column, :array? + assert_not_predicate @type, :binary? ratings_column = PgArray.columns_hash["ratings"] assert_equal :integer, ratings_column.type - assert ratings_column.array? + assert_predicate ratings_column, :array? end def test_not_compatible_with_serialize_array @@ -109,7 +109,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal :text, column.type assert_equal [], PgArray.column_defaults["snippets"] - assert column.array? + assert_predicate column, :array? end def test_change_column_cant_make_non_array_column_to_array @@ -228,7 +228,9 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_insert_fixtures tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + assert_deprecated do + @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + end assert_equal(PgArray.last.tags, tag_values) end @@ -255,7 +257,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x = PgArray.create!(tags: tags) x.reload - refute x.changed? + assert_not_predicate x, :changed? end def test_quoting_non_standard_delimiters @@ -277,7 +279,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x.reload assert_equal %w(one two three), x.tags - assert_not x.changed? + assert_not_predicate x, :changed? end def test_mutate_value_in_array @@ -288,7 +290,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x.reload assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores - assert_not x.changed? + assert_not_predicate x, :changed? end def test_datetime_with_timezone_awareness diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index df04299569..c8e728bbb6 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -29,20 +29,20 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlBitString.columns_hash["a_bit"] assert_equal :bit, column.type assert_equal "bit(8)", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlBitString.type_for_attribute("a_bit") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_bit_string_varying_column column = PostgresqlBitString.columns_hash["a_bit_varying"] assert_equal :bit_varying, column.type assert_equal "bit varying(4)", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlBitString.type_for_attribute("a_bit_varying") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_default diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index a6bee113ff..64bb6906cd 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -75,7 +75,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_write_value data = "\u001F" record = ByteaDataType.create(payload: data) - assert_not record.new_record? + assert_not_predicate record, :new_record? assert_equal(data, record.payload) end @@ -101,14 +101,14 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) assert(data.size > 1) record = ByteaDataType.create(payload: data) - assert_not record.new_record? + assert_not_predicate record, :new_record? assert_equal(data, record.payload) assert_equal(data, ByteaDataType.where(id: record.id).first.payload) end def test_write_nil record = ByteaDataType.create(payload: nil) - assert_not record.new_record? + assert_not_predicate record, :new_record? assert_nil(record.payload) assert_nil(ByteaDataType.where(id: record.id).first.payload) end diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index adf461a9cc..6dba4f3e14 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -33,7 +33,7 @@ module ActiveRecord connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp column = connection.columns(:strings).find { |c| c.name == "somedate" } assert_equal :datetime, column.type - assert column.array? + assert_predicate column, :array? end end end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index a25f102bad..9eb0b7d99c 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -32,10 +32,10 @@ class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase column = Citext.columns_hash["cival"] assert_equal :citext, column.type assert_equal "citext", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = Citext.type_for_attribute("cival") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_change_table_supports_json diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 5da95f7e2c..b0ce2694a3 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -51,10 +51,10 @@ class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlComposite.columns_hash["address"] assert_nil column.type assert_equal "full_address", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlComposite.type_for_attribute("address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_composite_mapping @@ -113,10 +113,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlComposite.columns_hash["address"] assert_equal :full_address, column.type assert_equal "full_address", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlComposite.type_for_attribute("address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_composite_mapping diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 81358b8fc4..d1b3c434e1 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -157,7 +157,7 @@ module ActiveRecord original_connection_pid = @connection.query("select pg_backend_pid()") # Sanity check. - assert @connection.active? + assert_predicate @connection, :active? if @connection.send(:postgresql_version) >= 90200 secondary_connection = ActiveRecord::Base.connection_pool.checkout @@ -176,7 +176,7 @@ module ActiveRecord @connection.verify! - assert @connection.active? + assert_predicate @connection, :active? # If we get no exception here, then either we re-connected successfully, or # we never actually got disconnected. diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 9c3817e2ad..eeaad94c27 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -30,10 +30,10 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlDomain.columns_hash["price"] assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlDomain.type_for_attribute("price") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_domain_acts_like_basetype @@ -44,6 +44,6 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase record.price = "34.15" record.save! - assert_equal BigDecimal.new("34.15"), record.reload.price + assert_equal BigDecimal("34.15"), record.reload.price end end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 3d3cbe11a3..6789ff63e7 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -32,10 +32,10 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlEnum.columns_hash["current_mood"] assert_equal :enum, column.type assert_equal "mood", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlEnum.type_for_attribute("current_mood") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_enum_defaults @@ -73,7 +73,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" stderr_output = capture(:stderr) { PostgresqlEnum.first } - assert stderr_output.blank? + assert_predicate stderr_output, :blank? end def test_enum_type_cast diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 16fec94ede..be525383e9 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true require "cases/helper" -require "models/developer" -require "models/computer" +require "models/author" +require "models/post" class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + explain = Author.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(QUERY PLAN), explain end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain + explain = Author.where(id: 1).includes(:posts).explain assert_match %(QUERY PLAN), explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain - assert_match %r(EXPLAIN for: SELECT "audit_logs"\.\* FROM "audit_logs" WHERE "audit_logs"\."developer_id" = (?:\$1 \[\["developer_id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb new file mode 100644 index 0000000000..4fa315ad23 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/professor" + +if ActiveRecord::Base.connection.supports_foreign_tables? + class ForeignTableTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class ForeignProfessor < ActiveRecord::Base + self.table_name = "foreign_professors" + end + + class ForeignProfessorWithPk < ForeignProfessor + self.primary_key = "id" + end + + def setup + @professor = Professor.create(name: "Nicola") + + @connection = ActiveRecord::Base.connection + enable_extension!("postgres_fdw", @connection) + + foreign_db_config = ARTest.connection_config["arunit2"] + @connection.execute <<-SQL + CREATE SERVER foreign_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '#{foreign_db_config["database"]}') + SQL + + @connection.execute <<-SQL + CREATE USER MAPPING FOR CURRENT_USER + SERVER foreign_server + SQL + + @connection.execute <<-SQL + CREATE FOREIGN TABLE foreign_professors ( + id int, + name character varying NOT NULL + ) SERVER foreign_server OPTIONS ( + table_name 'professors' + ) + SQL + end + + def teardown + disable_extension!("postgres_fdw", @connection) + @connection.execute <<-SQL + DROP SERVER IF EXISTS foreign_server CASCADE + SQL + end + + def test_table_exists + table_name = ForeignProfessor.table_name + assert_not ActiveRecord::Base.connection.table_exists?(table_name) + end + + def test_foreign_tables_are_valid_data_sources + table_name = ForeignProfessor.table_name + assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source" + end + + def test_foreign_tables + assert_equal ["foreign_professors"], @connection.foreign_tables + end + + def test_foreign_table_exists + assert @connection.foreign_table_exists?("foreign_professors") + assert @connection.foreign_table_exists?(:foreign_professors) + assert_not @connection.foreign_table_exists?("nonexistingtable") + assert_not @connection.foreign_table_exists?("'") + assert_not @connection.foreign_table_exists?(nil) + end + + def test_attribute_names + assert_equal ["id", "name"], ForeignProfessor.attribute_names + end + + def test_attributes + professor = ForeignProfessorWithPk.find(@professor.id) + assert_equal @professor.attributes, professor.attributes + end + + def test_does_not_have_a_primary_key + assert_nil ForeignProfessor.primary_key + end + + def test_insert_record + # Explicit `id` here to avoid complex configurations to implicitly work with remote table + ForeignProfessorWithPk.create!(id: 100, name: "Leonardo") + + professor = ForeignProfessorWithPk.last + assert_equal "Leonardo", professor.name + end + + def test_update_record + professor = ForeignProfessorWithPk.find(@professor.id) + professor.name = "Albert" + professor.save! + professor.reload + assert_equal "Albert", professor.name + end + + def test_delete_record + professor = ForeignProfessorWithPk.find(@professor.id) + assert_difference("ForeignProfessor.count", -1) { professor.destroy } + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index c6f1e1727f..95dee3bf44 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -22,10 +22,10 @@ class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase column = Tsvector.columns_hash["text_vector"] assert_equal :tsvector, column.type assert_equal "tsvector", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = Tsvector.type_for_attribute("text_vector") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_update_tsvector diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index e1ba00e07b..8c6f046553 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -39,10 +39,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlPoint.columns_hash["x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlPoint.type_for_attribute("x") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_default @@ -79,7 +79,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase p.reload assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x - assert_not p.changed? + assert_not_predicate p, :changed? end def test_array_assignment @@ -117,10 +117,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlPoint.columns_hash["legacy_x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlPoint.type_for_attribute("legacy_x") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_legacy_default @@ -157,7 +157,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase p.reload assert_equal [10.0, 25.0], p.legacy_x - assert_not p.changed? + assert_not_predicate p, :changed? end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index f09e34b5f2..4b061a9375 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -40,7 +40,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase end def test_hstore_included_in_extensions - assert @connection.respond_to?(:extensions), "connection should have a list of extensions" + assert_respond_to @connection, :extensions assert_includes @connection.extensions, "hstore", "extension list should include hstore" end @@ -58,9 +58,9 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase def test_column assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type - assert_not @column.array? + assert_not_predicate @column, :array? - assert_not @type.binary? + assert_not_predicate @type, :binary? end def test_default @@ -165,7 +165,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase hstore.reload assert_equal "four", hstore.settings["three"] - assert_not hstore.changed? + assert_not_predicate hstore, :changed? end def test_dirty_from_user_equal @@ -174,7 +174,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase hstore.settings = { "key" => "value", "alongkey" => "anything" } assert_equal settings, hstore.settings - refute hstore.changed? + assert_not_predicate hstore, :changed? end def test_hstore_dirty_from_database_equal @@ -184,7 +184,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase assert_equal settings, hstore.settings hstore.settings = settings - refute hstore.changed? + assert_not_predicate hstore, :changed? end def test_gen1 diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index eca29f2892..8349ee6ee2 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -31,10 +31,10 @@ class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase column = Ltree.columns_hash["path"] assert_equal :ltree, column.type assert_equal "ltree", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = Ltree.type_for_attribute("path") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_write diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 563f0bbfae..be3590e8dd 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -26,15 +26,15 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase assert_equal :money, column.type assert_equal "money", column.sql_type assert_equal 2, column.scale - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlMoney.type_for_attribute("wealth") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_default - assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults["depth"] - assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth + assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] + assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth end def test_money_values @@ -65,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase money = PostgresqlMoney.create(wealth: "987.65".dup) assert_equal 987.65, money.wealth - new_value = BigDecimal.new("123.45") + new_value = BigDecimal("123.45") money.wealth = new_value money.save! money.reload diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index f461544a85..736570451b 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -24,30 +24,30 @@ class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlNetworkAddress.columns_hash["cidr_address"] assert_equal :cidr, column.type assert_equal "cidr", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlNetworkAddress.type_for_attribute("cidr_address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_inet_column column = PostgresqlNetworkAddress.columns_hash["inet_address"] assert_equal :inet, column.type assert_equal "inet", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlNetworkAddress.type_for_attribute("inet_address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_macaddr_column column = PostgresqlNetworkAddress.columns_hash["mac_address"] assert_equal :macaddr, column.type assert_equal "macaddr", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = PostgresqlNetworkAddress.type_for_attribute("mac_address") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_network_types diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index f199519d86..1951230c8a 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -248,12 +248,12 @@ module ActiveRecord def test_index_with_opclass with_example_table do - @connection.add_index "ex", "data varchar_pattern_ops" - index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } - assert_equal "data varchar_pattern_ops", index.columns + @connection.add_index "ex", "data", opclass: "varchar_pattern_ops" + index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } + assert_equal ["data"], index.columns - @connection.remove_index "ex", "data varchar_pattern_ops" - assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } + @connection.remove_index "ex", "data" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index a75fdef698..261c24634e 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -134,10 +134,10 @@ _SQL end def test_numrange_values - assert_equal BigDecimal.new("0.1")..BigDecimal.new("0.2"), @first_range.num_range - assert_equal BigDecimal.new("0.1")...BigDecimal.new("0.2"), @second_range.num_range - assert_equal BigDecimal.new("0.1")...BigDecimal.new("Infinity"), @third_range.num_range - assert_equal BigDecimal.new("-Infinity")...BigDecimal.new("Infinity"), @fourth_range.num_range + assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range + assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range assert_nil @empty_range.num_range end @@ -285,14 +285,14 @@ _SQL def test_create_numrange assert_equal_round_trip(@new_range, :num_range, - BigDecimal.new("0.5")...BigDecimal.new("1")) + BigDecimal("0.5")...BigDecimal("1")) end def test_update_numrange assert_equal_round_trip(@first_range, :num_range, - BigDecimal.new("0.5")...BigDecimal.new("1")) + BigDecimal("0.5")...BigDecimal("1")) assert_nil_round_trip(@first_range, :num_range, - BigDecimal.new("0.5")...BigDecimal.new("0.5")) + BigDecimal("0.5")...BigDecimal("0.5")) end def test_create_daterange @@ -358,6 +358,18 @@ _SQL end end + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) + + record = PostgresqlRange.first + + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 5a64da028b..7ad03c194f 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -459,7 +459,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal :btree, index_d.using assert_equal :gin, index_e.using - assert_equal :desc, index_d.orders[INDEX_D_COLUMN] + assert_equal :desc, index_d.orders end end @@ -500,6 +500,66 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase end end +class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "trains" do |t| + t.string :name + t.text :description + end + end + + teardown do + @connection.drop_table "trains", if_exists: true + end + + def test_string_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: :text_pattern_ops/, output) + end + + def test_non_default_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) + end +end + +class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "trains" do |t| + t.string :name + t.text :description + end + end + + teardown do + @connection.drop_table "trains", if_exists: true + end + + def test_nulls_order_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name NULLS FIRST, description)" + output = dump_table_schema "trains" + assert_match(/order: \{ name: "NULLS FIRST" \}/, output) + end + + def test_non_default_order_with_nulls_is_dumped + @connection.execute "CREATE INDEX trains_name_and_desc ON trains USING btree(name DESC NULLS LAST, description)" + output = dump_table_schema "trains" + assert_match(/order: \{ name: "DESC NULLS LAST" \}/, output) + end +end + class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection @@ -534,7 +594,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa end def test_decimal_defaults_in_new_schema_when_overriding_domain - assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" + assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" end def test_bpchar_defaults_in_new_schema_when_overriding_domain diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index 6a99323be5..83ea86be6d 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -24,14 +24,14 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlSerial.columns_hash["seq"] assert_equal :integer, column.type assert_equal "integer", column.sql_type - assert column.serial? + assert_predicate column, :serial? end def test_not_serial_column column = PostgresqlSerial.columns_hash["serials_id"] assert_equal :integer, column.type assert_equal "integer", column.sql_type - assert_not column.serial? + assert_not_predicate column, :serial? end def test_schema_dump_with_shorthand @@ -66,14 +66,14 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase column = PostgresqlBigSerial.columns_hash["seq"] assert_equal :integer, column.type assert_equal "bigint", column.sql_type - assert column.serial? + assert_predicate column, :serial? end def test_not_bigserial_column column = PostgresqlBigSerial.columns_hash["serials_id"] assert_equal :integer, column.type assert_equal "bigint", column.sql_type - assert_not column.serial? + assert_not_predicate column, :serial? end def test_schema_dump_with_shorthand @@ -111,7 +111,7 @@ module SequenceNameDetectionTestCases columns = @connection.columns(:foo) columns.each do |column| assert_equal :integer, column.type - assert column.serial? + assert_predicate column, :serial? end end @@ -142,7 +142,7 @@ module SequenceNameDetectionTestCases columns = @connection.columns(@table_name) columns.each do |column| assert_equal :integer, column.type - assert column.serial? + assert_predicate column, :serial? end end diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index b6aec8e993..9821b103df 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -14,6 +14,7 @@ module ActiveRecord setup do @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) @connection = ActiveRecord::Base.connection @@ -31,6 +32,7 @@ module ActiveRecord @connection.drop_table "samples", if_exists: true Thread.abort_on_exception = @abort + Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) end test "raises SerializationFailure when a serialization failure occurs" do @@ -91,9 +93,9 @@ module ActiveRecord end end - test "raises TransactionTimeout when lock wait timeout exceeded" do + test "raises LockWaitTimeout when lock wait timeout exceeded" do skip unless ActiveRecord::Base.connection.postgresql_version >= 90300 - assert_raises(ActiveRecord::TransactionTimeout) do + assert_raises(ActiveRecord::LockWaitTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new latch2 = Concurrent::CountDownLatch.new @@ -120,6 +122,61 @@ module ActiveRecord end end + test "raises QueryCanceled when statement timeout exceeded" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET statement_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET statement_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'") + conn.execute("SELECT pg_cancel_backend(#{pid})") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end + private def with_warning_suppression diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index c24e0cb330..71d07e2f4c 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -82,7 +82,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] - assert column.array? + assert_predicate column, :array? assert_equal "{}", column.default schema = dump_table_schema "uuid_data_type" @@ -93,10 +93,10 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type assert_equal "uuid", column.sql_type - assert_not column.array? + assert_not_predicate column, :array? type = UUIDType.type_for_attribute("guid") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_treat_blank_uuid_as_nil @@ -178,7 +178,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase duplicate = klass.new(guid: record.guid) assert record.guid.present? # Ensure we actually are testing a UUID - assert_not duplicate.valid? + assert_not_predicate duplicate, :valid? end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 3b081d34e1..b6d2ccdb53 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true require "cases/helper" -require "models/developer" -require "models/computer" +require "models/author" +require "models/post" class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) + explain = Author.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %r(EXPLAIN for: SELECT "audit_logs"\.\* FROM "audit_logs" WHERE "audit_logs"\."developer_id" = (?:\? \[\["developer_id", 1\]\]|1)), explain - assert_match(/(SCAN )?TABLE audit_logs/, explain) + explain = Author.where(id: 1).includes(:posts).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain) + assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\? \[\["author_id", 1\]\]|1)), explain + assert_match(/(SCAN )?TABLE posts/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/json_test.rb b/activerecord/test/cases/adapters/sqlite3/json_test.rb index 568a524058..6f247fcd22 100644 --- a/activerecord/test/cases/adapters/sqlite3/json_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/json_test.rb @@ -9,8 +9,8 @@ class SQLite3JSONTest < ActiveRecord::SQLite3TestCase def setup super @connection.create_table("json_data_type") do |t| - t.column "payload", :json, default: {} - t.column "settings", :json + t.json "payload", default: {} + t.json "settings" end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index de422fad23..6fdb353368 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -38,7 +38,7 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase end def test_type_cast_bigdecimal - bd = BigDecimal.new "10.0" + bd = BigDecimal "10.0" assert_equal bd.to_f, @conn.type_cast(bd) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 1f057fe5c6..7e0ce3a28e 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -360,16 +360,126 @@ module ActiveRecord end end + class Barcode < ActiveRecord::Base + self.primary_key = "code" + end + + def test_copy_table_with_existing_records_have_custom_primary_key + connection = Barcode.connection + connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t| + t.text :other_attr + end + code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" + Barcode.create!(code: code, other_attr: "xxx") + + connection.remove_column("barcodes", "other_attr") + + assert_equal code, Barcode.first.id + ensure + Barcode.reset_column_information + end + + def test_copy_table_with_composite_primary_keys + connection = Barcode.connection + connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + t.string :region + t.string :code + t.text :other_attr + end + region = "US" + code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" + Barcode.create!(region: region, code: code, other_attr: "xxx") + + connection.remove_column("barcodes", "other_attr") + + assert_equal ["region", "code"], connection.primary_keys("barcodes") + + barcode = Barcode.first + assert_equal region, barcode.region + assert_equal code, barcode.code + ensure + Barcode.reset_column_information + end + + def test_custom_primary_key_in_create_table + connection = Barcode.connection + connection.create_table :barcodes, id: false, force: true do |t| + t.primary_key :id, :string + end + + assert_equal "id", connection.primary_key("barcodes") + + custom_pk = Barcode.columns_hash["id"] + + assert_equal :string, custom_pk.type + assert_not custom_pk.null + ensure + Barcode.reset_column_information + end + + def test_custom_primary_key_in_change_table + connection = Barcode.connection + connection.create_table :barcodes, id: false, force: true do |t| + t.integer :dummy + end + connection.change_table :barcodes do |t| + t.primary_key :id, :string + end + + assert_equal "id", connection.primary_key("barcodes") + + custom_pk = Barcode.columns_hash["id"] + + assert_equal :string, custom_pk.type + assert_not custom_pk.null + ensure + Barcode.reset_column_information + end + + def test_add_column_with_custom_primary_key + connection = Barcode.connection + connection.create_table :barcodes, id: false, force: true do |t| + t.integer :dummy + end + connection.add_column :barcodes, :id, :string, primary_key: true + + assert_equal "id", connection.primary_key("barcodes") + + custom_pk = Barcode.columns_hash["id"] + + assert_equal :string, custom_pk.type + assert_not custom_pk.null + ensure + Barcode.reset_column_information + end + + def test_remove_column_preserves_partial_indexes + connection = Barcode.connection + connection.create_table :barcodes, force: true do |t| + t.string :code + t.string :region + t.boolean :bool_attr + + t.index :code, unique: true, where: :bool_attr, name: "partial" + end + connection.remove_column :barcodes, :region + + index = connection.indexes("barcodes").find { |idx| idx.name == "partial" } + assert_equal "bool_attr", index.where + ensure + Barcode.reset_column_information + end + def test_supports_extensions assert_not @conn.supports_extensions?, "does not support extensions" end def test_respond_to_enable_extension - assert @conn.respond_to?(:enable_extension) + assert_respond_to @conn, :enable_extension end def test_respond_to_disable_extension - assert @conn.respond_to?(:disable_extension) + assert_respond_to @conn, :disable_extension end def test_statement_closed @@ -390,6 +500,10 @@ module ActiveRecord end end + def test_deprecate_valid_alter_table_type + assert_deprecated { @conn.valid_alter_table_type?(:string) } + end + private def assert_logged(logs) diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 7f654ec6f6..fbdf2ada4b 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -27,7 +27,7 @@ class AggregationsTest < ActiveRecord::TestCase def test_immutable_value_objects customers(:david).balance = Money.new(100) - assert_raise(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } } + assert_raise(frozen_error_class) { customers(:david).balance.instance_eval { @amount = 20 } } end def test_inferred_mapping diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 83974f327e..140d7cbcae 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -48,7 +48,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } - assert_equal 7, ActiveRecord::Migrator::current_version + assert_equal 7, @connection.migration_context.current_version end def test_schema_define_w_table_name_prefix @@ -64,7 +64,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase t.column :flavor, :string end end - assert_equal 7, ActiveRecord::Migrator::current_version + assert_equal 7, @connection.migration_context.current_version ensure ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 0f7a249bf3..9e6d94191b 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -79,7 +79,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - assert account.valid? + assert_predicate account, :valid? ensure ActiveRecord::Base.belongs_to_required_by_default = original_value end @@ -95,7 +95,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - assert_not account.valid? + assert_not_predicate account, :valid? assert_equal [{ error: :blank }], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -112,7 +112,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - assert_not account.valid? + assert_not_predicate account, :valid? assert_equal [{ error: :blank }], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -246,14 +246,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first - assert citibank_result.association(:firm_with_primary_key).loaded? + assert_predicate citibank_result.association(:firm_with_primary_key), :loaded? end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first - assert citibank_result.association(:firm_with_primary_key_symbols).loaded? + assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded? end def test_creating_the_belonging_object @@ -320,7 +320,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create!(name: "Jimmy") account = client.create_account!(credit_limit: 10) assert_equal account, client.account - assert account.persisted? + assert_predicate account, :persisted? client.save client.reload assert_equal account, client.account @@ -330,7 +330,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create!(name: "Jimmy") assert_raise(ActiveRecord::RecordInvalid) { client.create_account! } assert_not_nil client.account - assert client.account.new_record? + assert_predicate client.account, :new_record? end def test_reloading_the_belonging_object @@ -627,10 +627,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) final_cut.firm = firm - assert !final_cut.persisted? + assert_not_predicate final_cut, :persisted? assert final_cut.save - assert final_cut.persisted? - assert firm.persisted? + assert_predicate final_cut, :persisted? + assert_predicate firm, :persisted? assert_equal firm, final_cut.firm final_cut.association(:firm).reload assert_equal firm, final_cut.firm @@ -640,10 +640,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) final_cut.firm_with_primary_key = firm - assert !final_cut.persisted? + assert_not_predicate final_cut, :persisted? assert final_cut.save - assert final_cut.persisted? - assert firm.persisted? + assert_predicate final_cut, :persisted? + assert_predicate firm, :persisted? assert_equal firm, final_cut.firm_with_primary_key final_cut.association(:firm_with_primary_key).reload assert_equal firm, final_cut.firm_with_primary_key @@ -790,7 +790,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_cant_save_readonly_association assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! } - assert companies(:first_client).readonly_firm.readonly? + assert_predicate companies(:first_client).readonly_firm, :readonly? end def test_polymorphic_assignment_foreign_key_type_string @@ -949,15 +949,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase firm_proxy = client.send(:association_instance_get, :firm) firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition) - assert !firm_proxy.stale_target? - assert !firm_with_condition_proxy.stale_target? + assert_not_predicate firm_proxy, :stale_target? + assert_not_predicate firm_with_condition_proxy, :stale_target? assert_equal companies(:first_firm), client.firm assert_equal companies(:first_firm), client.firm_with_condition client.client_of = companies(:another_firm).id - assert firm_proxy.stale_target? - assert firm_with_condition_proxy.stale_target? + assert_predicate firm_proxy, :stale_target? + assert_predicate firm_with_condition_proxy, :stale_target? assert_equal companies(:another_firm), client.firm assert_equal companies(:another_firm), client.firm_with_condition end @@ -968,12 +968,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable proxy = sponsor.send(:association_instance_get, :sponsorable) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal members(:groucho), sponsor.sponsorable sponsor.sponsorable_id = members(:some_other_guy).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal members(:some_other_guy), sponsor.sponsorable end @@ -983,12 +983,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable proxy = sponsor.send(:association_instance_get, :sponsorable) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal members(:groucho), sponsor.sponsorable sponsor.sponsorable_type = "Firm" - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal companies(:first_firm), sponsor.sponsorable end @@ -1122,7 +1122,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase comment.post_id = 9223372036854775808 # out of range in the bigint assert_nil comment.post - assert_not comment.valid? + assert_not_predicate comment, :valid? assert_equal [{ error: :blank }], comment.errors.details[:post] end @@ -1142,7 +1142,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase citibank.firm_id = apple.id.to_s - assert !citibank.association(:firm).stale_target? + assert_not_predicate citibank.association(:firm), :stale_target? end def test_reflect_the_most_recent_change diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index e096cd4a0b..25d55dc4c9 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -15,7 +15,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase @david = authors(:david) @thinking = posts(:thinking) @authorless = posts(:authorless) - assert @david.post_log.empty? + assert_empty @david.post_log end def test_adding_macro_callbacks @@ -96,7 +96,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_add_callback david = developers(:david) ar = projects(:active_record) - assert ar.developers_log.empty? + assert_empty ar.developers_log ar.developers_with_callbacks << david assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log ar.developers_with_callbacks << david @@ -122,12 +122,12 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert_equal alice, dev assert_not_nil new_dev assert new_dev, "record should not have been saved" - assert_not alice.new_record? + assert_not_predicate alice, :new_record? end def test_has_and_belongs_to_many_after_add_called_after_save ar = projects(:active_record) - assert ar.developers_log.empty? + assert_empty ar.developers_log alice = Developer.new(name: "alice") ar.developers_with_callbacks << alice assert_equal "after_adding#{alice.id}", ar.developers_log.last @@ -143,7 +143,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase david = developers(:david) jamis = developers(:jamis) activerecord = projects(:active_record) - assert activerecord.developers_log.empty? + assert_empty activerecord.developers_log activerecord.developers_with_callbacks.delete(david) assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log @@ -154,7 +154,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear activerecord = projects(:active_record) - assert activerecord.developers_log.empty? + assert_empty activerecord.developers_log if activerecord.developers_with_callbacks.size == 0 activerecord.developers << developers(:david) activerecord.developers << developers(:jamis) @@ -163,7 +163,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase end activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort assert activerecord.developers_with_callbacks.clear - assert_predicate activerecord.developers_log, :empty? + assert_empty activerecord.developers_log end def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent @@ -183,7 +183,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase @david.unchangeable_posts << @authorless rescue Exception end - assert @david.post_log.empty? + assert_empty @david.post_log assert_not_includes @david.unchangeable_posts, @authorless @david.reload assert_not_includes @david.unchangeable_posts, @authorless diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 829e12fbc8..e717621928 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -136,7 +136,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_multiple_stis_and_order - author = Author.all.merge!(includes: { posts: [ :special_comments , :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first + author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first assert_equal authors(:david), author assert_no_queries do author.posts.first.special_comments diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 9afe6a893c..cb07ca5cd3 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -284,7 +284,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_from_an_association_that_has_a_hash_of_conditions - assert !Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? + assert_not_empty Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts end def test_loading_with_no_associations @@ -427,7 +427,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") assert_nothing_raised do - Comment.includes(:post).references(:posts).order(quoted_posts_id) + Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id)) end end @@ -787,7 +787,7 @@ class EagerAssociationTest < ActiveRecord::TestCase Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag) tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id) - assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)) + assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title) end def test_eager_has_many_through_multiple_with_order @@ -874,14 +874,14 @@ class EagerAssociationTest < ActiveRecord::TestCase posts(:thinking, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: "UPPER(posts.title)", limit: 2, offset: 1 + order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1 ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: "UPPER(posts.title) DESC", limit: 2, offset: 1 + order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1 ).to_a ) end @@ -891,14 +891,14 @@ class EagerAssociationTest < ActiveRecord::TestCase posts(:thinking, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: ["UPPER(posts.title)", "posts.id"], limit: 2, offset: 1 + order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1 ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: ["UPPER(posts.title) DESC", "posts.id"], limit: 2, offset: 1 + order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1 ).to_a ) end @@ -1073,7 +1073,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_load_with_sti_sharing_association - assert_queries(2) do #should not do 1 query per subclass + assert_queries(2) do # should not do 1 query per subclass Comment.includes(:post).to_a end end @@ -1444,51 +1444,51 @@ class EagerAssociationTest < ActiveRecord::TestCase test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! - assert firm.readonly_account.readonly? + assert_predicate firm.readonly_account, :readonly? # has_and_belongs_to_many project = Project.where(id: "2").preload(:readonly_developers).first! - assert project.readonly_developers.first.readonly? + assert_predicate project.readonly_developers.first, :readonly? # has-many :through david = Author.where(id: "1").preload(:readonly_comments).first! - assert david.readonly_comments.first.readonly? + assert_predicate david.readonly_comments.first, :readonly? end test "eager-loading non-readonly association" do # has_one firm = Firm.where(id: "1").eager_load(:account).first! - assert_not firm.account.readonly? + assert_not_predicate firm.account, :readonly? # has_and_belongs_to_many project = Project.where(id: "2").eager_load(:developers).first! - assert_not project.developers.first.readonly? + assert_not_predicate project.developers.first, :readonly? # has_many :through david = Author.where(id: "1").eager_load(:comments).first! - assert_not david.comments.first.readonly? + assert_not_predicate david.comments.first, :readonly? # belongs_to post = Post.where(id: "1").eager_load(:author).first! - assert_not post.author.readonly? + assert_not_predicate post.author, :readonly? end test "eager-loading readonly association" do # has-one firm = Firm.where(id: "1").eager_load(:readonly_account).first! - assert firm.readonly_account.readonly? + assert_predicate firm.readonly_account, :readonly? # has_and_belongs_to_many project = Project.where(id: "2").eager_load(:readonly_developers).first! - assert project.readonly_developers.first.readonly? + assert_predicate project.readonly_developers.first, :readonly? # has-many :through david = Author.where(id: "1").eager_load(:readonly_comments).first! - assert david.readonly_comments.first.readonly? + assert_predicate david.readonly_comments.first, :readonly? # belongs_to post = Post.where(id: "1").eager_load(:readonly_author).first! - assert post.readonly_author.readonly? + assert_predicate post.readonly_author, :readonly? end test "preloading a polymorphic association with references to the associated table" do 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 c817d7267b..d9e05cf7e2 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 @@ -180,11 +180,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many david = Developer.find(1) - assert !david.projects.empty? + assert_not_empty david.projects assert_equal 2, david.projects.size active_record = Project.find(1) - assert !active_record.developers.empty? + assert_not_empty active_record.developers assert_equal 3, active_record.developers.size assert_includes active_record.developers, david end @@ -262,10 +262,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase no_of_projects = Project.count aredridel = Developer.new("name" => "Aredridel") aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")]) - assert !aredridel.persisted? - assert !p.persisted? + assert_not_predicate aredridel, :persisted? + assert_not_predicate p, :persisted? assert aredridel.save - assert aredridel.persisted? + assert_predicate aredridel, :persisted? assert_equal no_of_devels + 1, Developer.count assert_equal no_of_projects + 1, Project.count assert_equal 2, aredridel.projects.size @@ -311,14 +311,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj - assert devel.projects.loaded? + assert_predicate devel.projects, :loaded? - assert !proj.persisted? + assert_not_predicate proj, :persisted? devel.save - assert proj.persisted? + assert_predicate proj, :persisted? assert_equal devel.projects.last, proj assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -326,14 +326,14 @@ 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") } - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj - assert devel.projects.loaded? + assert_predicate devel.projects, :loaded? - assert !proj.persisted? + assert_not_predicate proj, :persisted? devel.save - assert proj.persisted? + assert_predicate proj, :persisted? assert_equal devel.projects.last, proj assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -343,10 +343,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase devel.projects.build(name: "Make bed") proj2 = devel.projects.build(name: "Lie in it") assert_equal devel.projects.last, proj2 - assert !proj2.persisted? + assert_not_predicate proj2, :persisted? devel.save - assert devel.persisted? - assert proj2.persisted? + assert_predicate devel, :persisted? + assert_predicate proj2, :persisted? assert_equal devel.projects.last, proj2 assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated end @@ -354,12 +354,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_create devel = Developer.find(1) proj = devel.projects.create("name" => "Projekt") - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj - assert !devel.projects.loaded? + assert_not_predicate devel.projects, :loaded? - assert proj.persisted? + assert_predicate proj, :persisted? assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -367,14 +367,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # in Oracle '' is saved as null therefore need to save ' ' in not null column post = categories(:general).post_with_conditions.build(body: " ") - assert post.save - assert_equal "Yet Another Testing Title", post.title + assert post.save + assert_equal "Yet Another Testing Title", post.title # in Oracle '' is saved as null therefore need to save ' ' in not null column another_post = categories(:general).post_with_conditions.create(body: " ") - assert another_post.persisted? - assert_equal "Yet Another Testing Title", another_post.title + assert_predicate another_post, :persisted? + assert_equal "Yet Another Testing Title", another_post.title end def test_distinct_after_the_fact @@ -441,10 +441,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_removing_associations_on_destroy david = DeveloperWithBeforeDestroyRaise.find(1) - assert !david.projects.empty? + assert_not_empty david.projects david.destroy - assert david.projects.empty? - assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty? + assert_empty david.projects + assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1") end def test_destroying @@ -459,7 +459,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}") - assert join_records.empty? + assert_empty join_records assert_equal 1, david.reload.projects.size assert_equal 1, david.projects.reload.size @@ -475,7 +475,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}") - assert join_records.empty? + assert_empty join_records assert_equal 0, david.reload.projects.size assert_equal 0, david.projects.reload.size @@ -484,23 +484,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_destroy_all david = Developer.find(1) david.projects.reload - assert !david.projects.empty? + assert_not_empty david.projects assert_no_difference "Project.count" do david.projects.destroy_all end join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}") - assert join_records.empty? + assert_empty join_records - assert david.projects.empty? - assert david.projects.reload.empty? + assert_empty david.projects + assert_empty david.projects.reload end def test_destroy_associations_destroys_multiple_associations george = parrots(:george) - assert !george.pirates.empty? - assert !george.treasures.empty? + assert_not_empty george.pirates + assert_not_empty george.treasures assert_no_difference "Pirate.count" do assert_no_difference "Treasure.count" do @@ -509,12 +509,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") - assert join_records.empty? - assert george.pirates.reload.empty? + assert_empty join_records + assert_empty george.pirates.reload join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") - assert join_records.empty? - assert george.treasures.reload.empty? + assert_empty join_records + assert_empty george.treasures.reload end def test_associations_with_conditions @@ -547,7 +547,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first assert_no_queries(ignore_none: false) do - assert project.developers.loaded? + assert_predicate project.developers, :loaded? assert_includes project.developers, developer end end @@ -557,18 +557,18 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first project.reload - assert ! project.developers.loaded? + assert_not_predicate project.developers, :loaded? assert_queries(1) do assert_includes project.developers, developer end - assert ! project.developers.loaded? + assert_not_predicate project.developers, :loaded? end def test_include_returns_false_for_non_matching_record_to_verify_scoping project = projects(:active_record) developer = Developer.create name: "Bryan", salary: 50_000 - assert ! project.developers.loaded? + assert_not_predicate project.developers, :loaded? assert ! project.developers.include?(developer) end @@ -763,9 +763,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them developer = developers(:david) - assert !developer.projects.loaded? + assert_not_predicate developer.projects, :loaded? assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort - assert !developer.projects.loaded? + assert_not_predicate developer.projects, :loaded? end def test_assign_ids diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 4ca11af791..9ee42cef0f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -57,7 +57,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase def test_custom_primary_key_on_new_record_should_fetch_with_query subscriber = Subscriber.new(nick: "webster132") - assert !subscriber.subscriptions.loaded? + assert_not_predicate subscriber.subscriptions, :loaded? assert_queries 1 do assert_equal 2, subscriber.subscriptions.size @@ -68,7 +68,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase def test_association_primary_key_on_new_record_should_fetch_with_query author = Author.new(name: "David") - assert !author.essays.loaded? + assert_not_predicate author.essays, :loaded? assert_queries 1 do assert_equal 1, author.essays.size @@ -103,7 +103,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase def test_blank_custom_primary_key_on_new_record_should_not_run_queries author = Author.new - assert !author.essays.loaded? + assert_not_predicate author.essays, :loaded? assert_queries 0 do assert_equal 0, author.essays.size @@ -201,7 +201,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase part.reload assert_nil part.ship - assert !part.updated_at_changed? + assert_not_predicate part, :updated_at_changed? end def test_create_from_association_should_respect_default_scope @@ -444,7 +444,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_clients << company.clients_of_firm.build(name: "Another Client III") end - assert_not company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_queries(1) do assert_same new_clients[0], company.clients_of_firm.third assert_same new_clients[1], company.clients_of_firm.fourth @@ -464,7 +464,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_clients << company.clients_of_firm.build(name: "Another Client III") end - assert_not company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_queries(1) do assert_same new_clients[0], company.clients_of_firm.third! assert_same new_clients[1], company.clients_of_firm.fourth! @@ -587,14 +587,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # taking from unloaded Relation bob = klass.find(authors(:bob).id) new_post = bob.posts.build - assert_not bob.posts.loaded? + assert_not_predicate bob.posts, :loaded? assert_equal [posts(:misc_by_bob)], bob.posts.take(1) assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3) # taking from loaded Relation bob.posts.load - assert bob.posts.loaded? + assert_predicate bob.posts, :loaded? assert_equal [posts(:misc_by_bob)], bob.posts.take(1) assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3) @@ -713,13 +713,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_each firm = companies(:first_firm) - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries(4) do firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id } end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_find_each_with_conditions @@ -732,13 +732,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_find_in_batches firm = companies(:first_firm) - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries(2) do firm.clients.find_in_batches(batch_size: 2) do |clients| @@ -746,7 +746,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_find_all_sanitized @@ -955,20 +955,20 @@ 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") } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal new_client, company.clients_of_firm.last end def test_build company = companies(:first_firm) new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal new_client, company.clients_of_firm.last end @@ -982,9 +982,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_collection_not_empty_after_building company = companies(:first_firm) - assert_predicate company.contracts, :empty? + assert_empty company.contracts company.contracts.build - assert_not_predicate company.contracts, :empty? + assert_not_empty company.contracts end def test_collection_size_twice_for_regressions @@ -1008,7 +1008,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_followed_by_save_does_not_load_target companies(:first_firm).clients_of_firm.build("name" => "Another Client") assert companies(:first_firm).save - assert !companies(:first_firm).clients_of_firm.loaded? + assert_not_predicate companies(:first_firm).clients_of_firm, :loaded? end def test_build_without_loading_association @@ -1028,10 +1028,10 @@ 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" } } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal new_client, company.clients_of_firm.last end @@ -1069,7 +1069,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_predicate companies(:first_firm).clients_of_firm, :loaded? new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert new_client.persisted? + assert_predicate new_client, :persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last end @@ -1082,7 +1082,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_followed_by_save_does_not_load_target companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert companies(:first_firm).save - assert !companies(:first_firm).clients_of_firm.loaded? + assert_not_predicate companies(:first_firm).clients_of_firm, :loaded? end def test_deleting @@ -1108,7 +1108,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # option is not given on the association. ship = Ship.create(name: "Countless", treasures_count: 10) - assert_not Ship.reflect_on_association(:treasures).has_cached_counter? + assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter? # Count should come from sql count() of treasures rather than treasures_count attribute assert_equal ship.treasures.size, 0 @@ -1199,7 +1199,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_empty_with_counter_cache post = posts(:welcome) assert_queries(0) do - assert_not post.comments.empty? + assert_not_empty post.comments end end @@ -1441,13 +1441,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_creation_respects_hash_condition ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build - assert ms_client.save - assert_equal "Microsoft", ms_client.name + assert ms_client.save + assert_equal "Microsoft", ms_client.name another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create - assert another_ms_client.persisted? - assert_equal "Microsoft", another_ms_client.name + assert_predicate another_ms_client, :persisted? + assert_equal "Microsoft", another_ms_client.name end def test_clearing_without_initial_access @@ -1570,7 +1570,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal 3, firm.clients.size firm.destroy - assert Client.all.merge!(where: "firm_id=#{firm.id}").to_a.empty? + assert_empty Client.all.merge!(where: "firm_id=#{firm.id}").to_a end def test_dependence_for_associations_with_hash_condition @@ -1633,7 +1633,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = RestrictedWithExceptionFirm.create!(name: "restrict") firm.companies.create(name: "child") - assert !firm.companies.empty? + assert_not_empty firm.companies assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedWithExceptionFirm.exists?(name: "restrict") assert firm.companies.exists?(name: "child") @@ -1643,11 +1643,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") - assert !firm.companies.empty? + assert_not_empty firm.companies firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") @@ -1660,11 +1660,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") - assert !firm.companies.empty? + assert_not_empty firm.companies firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") @@ -1716,8 +1716,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase account = Account.new orig_accounts = firm.accounts.to_a - assert !account.valid? - assert !orig_accounts.empty? + assert_not_predicate account, :valid? + assert_not_empty orig_accounts error = assert_raise ActiveRecord::RecordNotSaved do firm.accounts = [account] end @@ -1775,9 +1775,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them company = companies(:first_firm) - assert !company.clients.loaded? + assert_not_predicate company.clients, :loaded? assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids - assert !company.clients.loaded? + assert_not_predicate company.clients, :loaded? end def test_counter_cache_on_unloaded_association @@ -1859,7 +1859,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client = firm.clients.first assert_no_queries do - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? assert_equal true, firm.clients.include?(client) end end @@ -1869,18 +1869,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client = firm.clients.first firm.reload - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries(1) do assert_equal true, firm.clients.include?(client) end - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_include_returns_false_for_non_matching_record_to_verify_scoping firm = companies(:first_firm) client = Client.create!(name: "Not Associated") - assert ! firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_equal false, firm.clients.include?(client) end @@ -1889,13 +1889,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.first firm.clients.second firm.clients.last - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query firm = companies(:first_firm) firm.clients.load_target - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? assert_no_queries(ignore_none: false) do firm.clients.first @@ -1908,7 +1908,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_on_existing_record_with_build_should_load_association firm = companies(:first_firm) firm.clients.build(name: "Foo") - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries 1 do firm.clients.first @@ -1916,13 +1916,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.last end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association firm = companies(:first_firm) firm.clients.create(name: "Foo") - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries 3 do firm.clients.first @@ -1930,7 +1930,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.last end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_first_nth_or_last_on_new_record_should_not_run_queries @@ -1946,14 +1946,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.create(name: "Foo") - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? assert_queries 2 do firm.clients.first(2) firm.clients.last(2) end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_many_should_count_instead_of_loading_association @@ -1961,7 +1961,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(1) do firm.clients.many? # use count query end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_many_on_loaded_association_should_not_use_query @@ -1976,22 +1976,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.expects(:size).never firm.clients.many? { true } end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_many_should_return_false_if_none_or_one firm = companies(:another_firm) - assert !firm.clients_like_ms.many? + assert_not_predicate firm.clients_like_ms, :many? assert_equal 0, firm.clients_like_ms.size firm = companies(:first_firm) - assert !firm.limited_clients.many? + assert_not_predicate firm.limited_clients, :many? assert_equal 1, firm.limited_clients.size end def test_calling_many_should_return_true_if_more_than_one firm = companies(:first_firm) - assert firm.clients.many? + assert_predicate firm.clients, :many? assert_equal 3, firm.clients.size end @@ -2000,7 +2000,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(1) do firm.clients.none? # use count query end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_none_on_loaded_association_should_not_use_query @@ -2015,18 +2015,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.expects(:size).never firm.clients.none? { true } end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_none_should_return_true_if_none firm = companies(:another_firm) - assert firm.clients_like_ms.none? + assert_predicate firm.clients_like_ms, :none? assert_equal 0, firm.clients_like_ms.size end def test_calling_none_should_return_false_if_any firm = companies(:first_firm) - assert !firm.limited_clients.none? + assert_not_predicate firm.limited_clients, :none? assert_equal 1, firm.limited_clients.size end @@ -2035,7 +2035,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(1) do firm.clients.one? # use count query end - assert !firm.clients.loaded? + assert_not_predicate firm.clients, :loaded? end def test_calling_one_on_loaded_association_should_not_use_query @@ -2050,24 +2050,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.expects(:size).never firm.clients.one? { true } end - assert firm.clients.loaded? + assert_predicate firm.clients, :loaded? end def test_calling_one_should_return_false_if_zero firm = companies(:another_firm) - assert ! firm.clients_like_ms.one? + assert_not_predicate firm.clients_like_ms, :one? assert_equal 0, firm.clients_like_ms.size end def test_calling_one_should_return_true_if_one firm = companies(:first_firm) - assert firm.limited_clients.one? + assert_predicate firm.limited_clients, :one? assert_equal 1, firm.limited_clients.size end def test_calling_one_should_return_false_if_more_than_one firm = companies(:first_firm) - assert ! firm.clients.one? + assert_not_predicate firm.clients, :one? assert_equal 3, firm.clients.size end @@ -2287,7 +2287,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) assert post.taggings_with_delete_all.count > 0 - assert !post.taggings_with_delete_all.loaded? + assert_not_predicate post.taggings_with_delete_all, :loaded? # 2 queries: one DELETE and another to update the counter cache assert_queries(2) do @@ -2309,7 +2309,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "collection proxy respects default scope" do author = authors(:mary) - assert !author.first_posts.exists? + assert_not_predicate author.first_posts, :exists? end test "association with extend option" do @@ -2350,7 +2350,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "does not duplicate associations when used with natural primary keys" do speedometer = Speedometer.create!(id: "4") - speedometer.minivans.create!(minivan_id: "a-van-red" , name: "a van", color: "red") + speedometer.minivans.create!(minivan_id: "a-van-red", name: "a van", color: "red") assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}" assert_equal 1, speedometer.reload.minivans.to_a.size @@ -2428,7 +2428,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase pirate = FamousPirate.new pirate.famous_ships << ship = FamousShip.new - assert pirate.valid? + assert_predicate pirate, :valid? assert_not pirate.valid?(:conference) assert_equal "can't be blank", ship.errors[:name].first end @@ -2509,6 +2509,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_same car, new_bulb.car end + test "reattach to new objects replaces inverse association and foreign key" do + bulb = Bulb.create!(car: Car.create!) + assert bulb.car_id + car = Car.new + car.bulbs << bulb + assert_equal car, bulb.car + assert_nil bulb.car_id + end + test "in memory replacement maintains order" do first_bulb = Bulb.create! second_bulb = Bulb.create! @@ -2520,6 +2529,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + test "association size calculation works with default scoped selects when not previously fetched" do + firm = Firm.create!(name: "Firm") + 5.times { firm.developers_with_select << Developer.create!(name: "Developer") } + + same_firm = Firm.find(firm.id) + assert_equal 5, same_firm.developers_with_select.size + end + test "prevent double insertion of new object when the parent association loaded in the after save callback" do reset_callbacks(:save, Bulb) do Bulb.after_save { |record| record.car.bulbs.load } 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 046020e310..3b3d4037b9 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -46,6 +46,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create person_id: 0, post_id: 0 end + def test_marshal_dump + preloaded = Post.includes(:first_blue_tags).first + assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) + end + def test_preload_sti_rhs_class developers = Developer.includes(:firms).all.to_a assert_no_queries do @@ -353,10 +358,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert_queries(1) do - assert posts(:welcome).people.empty? + assert_empty posts(:welcome).people end - assert posts(:welcome).reload.people.reload.empty? + assert_empty posts(:welcome).reload.people.reload end def test_destroy_association @@ -366,8 +371,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people.reload.empty? + assert_empty posts(:welcome).reload.people + assert_empty posts(:welcome).people.reload end def test_destroy_all @@ -377,8 +382,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people.reload.empty? + assert_empty posts(:welcome).reload.people + assert_empty posts(:welcome).people.reload end def test_should_raise_exception_for_destroying_mismatching_records @@ -538,6 +543,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_update_counter_caches_on_destroy_with_indestructible_through_record + post = posts(:welcome) + tag = post.indestructible_tags.create!(name: "doomed") + post.update_columns(indestructible_tags_count: post.indestructible_tags.count) + + assert_no_difference "post.reload.indestructible_tags_count" do + posts(:welcome).indestructible_tags.destroy(tag) + end + end + def test_replace_association assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload } @@ -675,10 +690,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert_queries(0) do - assert posts(:welcome).people.empty? + assert_empty posts(:welcome).people end - assert posts(:welcome).reload.people.reload.empty? + assert_empty posts(:welcome).reload.people.reload end def test_association_callback_ordering @@ -760,9 +775,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them person = people(:michael) - assert !person.posts.loaded? + assert_not_predicate person.posts, :loaded? assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort - assert !person.posts.loaded? + assert_not_predicate person.posts, :loaded? end def test_association_proxy_transaction_method_starts_transaction_in_association_class @@ -852,7 +867,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.create(name: "Primary") author.named_categories.delete(category) assert !Categorization.exists?(author_id: author.id, named_category_name: category.name) - assert author.named_categories.reload.empty? + assert_empty author.named_categories.reload end def test_collection_singular_ids_getter_with_string_primary_keys @@ -869,6 +884,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [dev], company.developers end + def test_collection_singular_ids_setter_with_required_type_cast + company = companies(:rails_core) + dev = Developer.first + + company.developer_ids = [dev.id.to_s] + assert_equal [dev], company.developers + end + def test_collection_singular_ids_setter_with_string_primary_keys assert_nothing_raised do book = books(:awdr) @@ -926,8 +949,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_through_association_readonly_should_be_false - assert !people(:michael).posts.first.readonly? - assert !people(:michael).posts.to_a.first.readonly? + assert_not_predicate people(:michael).posts.first, :readonly? + assert_not_predicate people(:michael).posts.to_a.first, :readonly? end def test_can_update_through_association @@ -1016,12 +1039,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post.author_categorizations proxy = post.send(:association_instance_get, :author_categorizations) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) post.author_id = authors(:david).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) end @@ -1038,7 +1061,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_includes post.author_addresses, address post.author_addresses.delete(address) - assert post[:author_count].nil? + assert_predicate post[:author_count], :nil? end def test_primary_key_option_on_source @@ -1300,6 +1323,70 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_many_through_update_ids_with_conditions + author = Author.create!(name: "Bill") + category = categories(:general) + + author.update( + special_categories_with_condition_ids: [category.id], + nonspecial_categories_with_condition_ids: [category.id] + ) + + assert_equal [category.id], author.special_categories_with_condition_ids + assert_equal [category.id], author.nonspecial_categories_with_condition_ids + + author.update(nonspecial_categories_with_condition_ids: []) + author.reload + + assert_equal [category.id], author.special_categories_with_condition_ids + assert_equal [], author.nonspecial_categories_with_condition_ids + end + + def test_single_has_many_through_association_with_unpersisted_parent_instance + post_with_single_has_many_through = Class.new(Post) do + def self.name; "PostWithSingleHasManyThrough"; end + has_many :subscriptions, through: :author + end + post = post_with_single_has_many_through.new + + post.author = authors(:mary) + book1 = Book.create!(name: "essays on single has many through associations 1") + post.author.books << book1 + subscription1 = Subscription.first + book1.subscriptions << subscription1 + assert_equal [subscription1], post.subscriptions.to_a + + post.author = authors(:bob) + book2 = Book.create!(name: "essays on single has many through associations 2") + post.author.books << book2 + subscription2 = Subscription.second + book2.subscriptions << subscription2 + assert_equal [subscription2], post.subscriptions.to_a + end + + def test_nested_has_many_through_association_with_unpersisted_parent_instance + post_with_nested_has_many_through = Class.new(Post) do + def self.name; "PostWithNestedHasManyThrough"; end + has_many :books, through: :author + has_many :subscriptions, through: :books + end + post = post_with_nested_has_many_through.new + + post.author = authors(:mary) + book1 = Book.create!(name: "essays on nested has many through associations 1") + post.author.books << book1 + subscription1 = Subscription.first + book1.subscriptions << subscription1 + assert_equal [subscription1], post.subscriptions.to_a + + post.author = authors(:bob) + book2 = Book.create!(name: "essays on nested has many through associations 2") + post.author.books << book2 + subscription2 = Subscription.second + book2.subscriptions << subscription2 + assert_equal [subscription2], post.subscriptions.to_a + end + private def make_model(name) Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index ec5d95080b..1a213ef7e4 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -114,8 +114,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase developer = Developer.create!(name: "Someone") ship = Ship.create!(name: "Planet Caravan", developer: developer) ship.destroy - assert !ship.persisted? - assert !developer.persisted? + assert_not_predicate ship, :persisted? + assert_not_predicate developer, :persisted? end def test_natural_assignment_to_nil_after_destroy @@ -186,7 +186,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedWithExceptionFirm.exists?(name: "restrict") - assert firm.account.present? + assert_predicate firm.account, :present? end def test_restrict_with_error @@ -197,10 +197,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.account.present? + assert_predicate firm.account, :present? end def test_restrict_with_error_with_locale @@ -213,10 +213,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy - assert !firm.errors.empty? + assert_not_empty firm.errors assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.account.present? + assert_predicate firm.account, :present? ensure I18n.backend.reload! end @@ -377,7 +377,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_assignment_before_child_saved firm = Firm.find(1) firm.account = a = Account.new("credit_limit" => 1000) - assert a.persisted? + assert_predicate a, :persisted? assert_equal a, firm.account assert_equal a, firm.account firm.association(:account).reload @@ -395,7 +395,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_cant_save_readonly_association assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! } - assert companies(:first_firm).readonly_account.readonly? + assert_predicate companies(:first_firm).readonly_account, :readonly? end def test_has_one_proxy_should_not_respond_to_private_methods @@ -433,7 +433,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_create_respects_hash_condition account = companies(:first_firm).create_account_limit_500_with_hash_conditions - assert account.persisted? + assert_predicate account, :persisted? assert_equal 500, account.credit_limit end @@ -450,7 +450,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase new_ship = pirate.create_ship assert_not_equal ships(:black_pearl), new_ship assert_equal new_ship, pirate.ship - assert new_ship.new_record? + assert_predicate new_ship, :new_record? assert_nil orig_ship.pirate_id assert !orig_ship.changed? # check it was saved end @@ -460,8 +460,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase orig_ship = pirate.dependent_ship new_ship = pirate.create_dependent_ship - assert new_ship.new_record? - assert orig_ship.destroyed? + assert_predicate new_ship, :new_record? + assert_predicate orig_ship, :destroyed? end def test_creation_failure_due_to_new_record_should_raise_error @@ -481,7 +481,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase pirate = pirates(:blackbeard) pirate.ship.name = nil - assert !pirate.ship.valid? + assert_not_predicate pirate.ship, :valid? error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = ships(:interceptor) end @@ -588,7 +588,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase ship.save! ship.name = "new name" - assert ship.changed? + assert_predicate ship, :changed? assert_queries(1) do # One query for updating name, not triggering query for updating pirate_id pirate.ship = ship diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index fe24c465b2..9964f084ac 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -42,6 +42,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil new_member.club end + def test_creating_association_builds_through_record + new_member = Member.create(name: "Chris") + new_club = new_member.association(:club).build + assert new_member.current_membership + assert_equal new_club, new_member.club + assert_predicate new_club, :new_record? + assert_predicate new_member.current_membership, :new_record? + assert new_member.save + assert_predicate new_club, :persisted? + assert_predicate new_member.current_membership, :persisted? + end + def test_creating_association_builds_through_record_for_new new_member = Member.new(name: "Jane") new_member.club = clubs(:moustache_club) @@ -100,7 +112,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_has_one_through_eager_loading - members = assert_queries(3) do #base table, through table, clubs table + members = assert_queries(3) do # base table, through table, clubs table Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size @@ -108,7 +120,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_has_one_through_eager_loading_through_polymorphic - members = assert_queries(3) do #base table, through table, clubs table + members = assert_queries(3) do # base table, through table, clubs table Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size @@ -139,7 +151,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eagerloading members = assert_queries(1) do - Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback + Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].club } @@ -147,7 +159,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic members = assert_queries(1) do - Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback + Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } @@ -156,7 +168,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save! members = assert_queries(1) do - Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a #force fallback + Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } @@ -229,7 +241,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase MemberDetail.all.merge!(includes: :member_type).to_a end @new_detail = @member_details[0] - assert @new_detail.send(:association, :member_type).loaded? + assert_predicate @new_detail.send(:association, :member_type), :loaded? assert_no_queries { @new_detail.member_type } end @@ -317,12 +329,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase minivan.dashboard proxy = minivan.send(:association_instance_get, :dashboard) - assert !proxy.stale_target? + assert_not_predicate proxy, :stale_target? assert_equal dashboards(:cool_first), minivan.dashboard minivan.speedometer_id = speedometers(:second).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal dashboards(:second), minivan.dashboard end @@ -334,7 +346,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase minivan.speedometer_id = speedometers(:second).id - assert proxy.stale_target? + assert_predicate proxy, :stale_target? assert_equal dashboards(:second), minivan.dashboard end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 7be875fec6..ca0620dc3b 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -115,19 +115,19 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase scope = Post.joins(:special_comments).where(id: posts(:sti_comments).id) # The join should match SpecialComment and its subclasses only - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? - assert !scope.where("comments.type" => "SubSpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") + assert_not_empty scope.where("comments.type" => "SubSpecialComment") end def test_find_with_conditions_on_reflection - assert !posts(:welcome).comments.empty? + assert_not_empty posts(:welcome).comments assert Post.joins(:nonexistent_comments).where(id: posts(:welcome).id).empty? # [sic!] end def test_find_with_conditions_on_through_reflection - assert !posts(:welcome).tags.empty? - assert Post.joins(:misc_tags).where(id: posts(:welcome).id).empty? + assert_not_empty posts(:welcome).tags + assert_empty Post.joins(:misc_tags).where(id: posts(:welcome).id) end test "the default scope of the target is applied when joining associations" do diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index e13cf93dcf..bad1bcdb67 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -129,7 +129,7 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase def test_polymorphic_has_one_should_find_inverse_automatically man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) - assert man_reflection.has_inverse? + assert_predicate man_reflection, :has_inverse? end end @@ -150,22 +150,22 @@ class InverseAssociationTests < ActiveRecord::TestCase def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse has_one_with_inverse_ref = Man.reflect_on_association(:face) - assert has_one_with_inverse_ref.has_inverse? + assert_predicate has_one_with_inverse_ref, :has_inverse? has_many_with_inverse_ref = Man.reflect_on_association(:interests) - assert has_many_with_inverse_ref.has_inverse? + assert_predicate has_many_with_inverse_ref, :has_inverse? belongs_to_with_inverse_ref = Face.reflect_on_association(:man) - assert belongs_to_with_inverse_ref.has_inverse? + assert_predicate belongs_to_with_inverse_ref, :has_inverse? has_one_without_inverse_ref = Club.reflect_on_association(:sponsor) - assert !has_one_without_inverse_ref.has_inverse? + assert_not_predicate has_one_without_inverse_ref, :has_inverse? has_many_without_inverse_ref = Club.reflect_on_association(:memberships) - assert !has_many_without_inverse_ref.has_inverse? + assert_not_predicate has_many_without_inverse_ref, :has_inverse? belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club) - assert !belongs_to_without_inverse_ref.has_inverse? + assert_not_predicate belongs_to_without_inverse_ref, :has_inverse? end def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of @@ -464,7 +464,7 @@ class InverseHasManyTests < ActiveRecord::TestCase interest = Interest.create!(man: man) man.interests.find(interest.id) - assert_not man.interests.loaded? + assert_not_predicate man.interests, :loaded? end def test_raise_record_not_found_error_when_invalid_ids_are_passed @@ -484,7 +484,10 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_raise_record_not_found_error_when_no_ids_are_passed man = Man.create! - assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() } + exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() } + + assert_equal exception.model, "Interest" + assert_equal exception.primary_key, "id" end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error @@ -501,16 +504,16 @@ class InverseHasManyTests < ActiveRecord::TestCase i.man.name = "Charles" assert_equal i.man.name, man.name - assert !man.persisted? + assert_not_predicate man, :persisted? end def test_inverse_instance_should_be_set_before_find_callbacks_are_run reset_callbacks(Interest, :find) do Interest.after_find { raise unless association(:man).loaded? && man.present? } - assert Man.first.interests.reload.any? - assert Man.includes(:interests).first.interests.any? - assert Man.joins(:interests).includes(:interests).first.interests.any? + assert_predicate Man.first.interests.reload, :any? + assert_predicate Man.includes(:interests).first.interests, :any? + assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any? end end @@ -518,9 +521,9 @@ class InverseHasManyTests < ActiveRecord::TestCase reset_callbacks(Interest, :initialize) do Interest.after_initialize { raise unless association(:man).loaded? && man.present? } - assert Man.first.interests.reload.any? - assert Man.includes(:interests).first.interests.any? - assert Man.joins(:interests).includes(:interests).first.interests.any? + assert_predicate Man.first.interests.reload, :any? + assert_predicate Man.includes(:interests).first.interests, :any? + assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any? end end @@ -672,6 +675,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal old_inversed_man.object_id, new_inversed_man.object_id end + def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation + face = Face.new man: Man.new + + old_inversed_man = face.man + face.save! + new_inversed_man = face.man + + assert_equal old_inversed_man.object_id, new_inversed_man.object_id + end + def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many i = interests(:llama_wrangling) m = i.polymorphic_man diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 87694b0788..f19a9f5f7a 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -44,11 +44,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_distinct_through_count author = authors(:mary) - assert !authors(:mary).unique_categorized_posts.loaded? + assert_not_predicate authors(:mary).unique_categorized_posts, :loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) } assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) } - assert !authors(:mary).unique_categorized_posts.loaded? + assert_not_predicate authors(:mary).unique_categorized_posts, :loaded? end def test_has_many_distinct_through_find @@ -454,8 +454,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_uses_conditions_specified_on_the_has_many_association author = Author.first - assert author.comments.present? - assert author.nonexistent_comments.blank? + assert_predicate author.comments, :present? + assert_predicate author.nonexistent_comments, :blank? end def test_has_many_through_uses_correct_attributes @@ -467,27 +467,27 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_tag = Tag.new(name: "new") saved_post.tags << new_tag - assert new_tag.persisted? #consistent with habtm! - assert saved_post.persisted? + assert new_tag.persisted? # consistent with habtm! + assert_predicate saved_post, :persisted? assert_includes saved_post.tags, new_tag - assert new_tag.persisted? + assert_predicate new_tag, :persisted? assert_includes saved_post.reload.tags.reload, new_tag new_post = Post.new(title: "Association replacement works!", body: "You best believe it.") saved_tag = tags(:general) new_post.tags << saved_tag - assert !new_post.persisted? - assert saved_tag.persisted? + assert_not_predicate new_post, :persisted? + assert_predicate saved_tag, :persisted? assert_includes new_post.tags, saved_tag new_post.save! - assert new_post.persisted? + assert_predicate new_post, :persisted? assert_includes new_post.reload.tags.reload, saved_tag - assert !posts(:thinking).tags.build.persisted? - assert !posts(:thinking).tags.new.persisted? + assert_not_predicate posts(:thinking).tags.build, :persisted? + assert_not_predicate posts(:thinking).tags.new, :persisted? end def test_create_associate_when_adding_to_has_many_through @@ -529,14 +529,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded author = authors(:david) assert_equal 10, author.comments.size - assert !author.comments.loaded? + assert_not_predicate author.comments, :loaded? end def test_has_many_through_collection_size_uses_counter_cache_if_it_exists c = categories(:general) c.categorizations_count = 100 assert_equal 100, c.categorizations.size - assert !c.categorizations.loaded? + assert_not_predicate c.categorizations, :loaded? end def test_adding_junk_to_has_many_through_should_raise_type_mismatch @@ -710,7 +710,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = david.categories.first assert_no_queries do - assert david.categories.loaded? + assert_predicate david.categories, :loaded? assert_includes david.categories, category end end @@ -720,18 +720,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = david.categories.first david.reload - assert ! david.categories.loaded? + assert_not_predicate david.categories, :loaded? assert_queries(1) do assert_includes david.categories, category end - assert ! david.categories.loaded? + assert_not_predicate david.categories, :loaded? end def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping david = authors(:david) category = Category.create!(name: "Not Associated") - assert ! david.categories.loaded? + assert_not_predicate david.categories, :loaded? assert ! david.categories.include?(category) end diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb index c95d0425cd..7b5c394177 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -69,15 +69,15 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id) # The join should match SpecialComment and its subclasses only - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? - assert !scope.where("comments.type" => "SubSpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") + assert_not_empty scope.where("comments.type" => "SubSpecialComment") end def test_does_not_override_select authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts) - assert authors.any? - assert authors.first.respond_to?(:addr_id) + assert_predicate authors, :any? + assert_respond_to authors.first, :addr_id end test "the default scope of the target is applied when joining associations" do diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 65d30d011b..03ed1c1d47 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -78,7 +78,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # This ensures that the polymorphism of taggings is being observed correctly authors = Author.joins(:tags).where("taggings.taggable_type" => "FakeModel") - assert authors.empty? + assert_empty authors end # has_many through @@ -177,7 +177,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase members = Member.joins(:organization_member_details). where("member_details.id" => 9) - assert members.empty? + assert_empty members end # has_many through @@ -209,7 +209,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase members = Member.joins(:organization_member_details_2). where("member_details.id" => 9) - assert members.empty? + assert_empty members end # has_many through @@ -425,9 +425,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Check the polymorphism of taggings is being observed correctly (in both joins) authors = Author.joins(:similar_posts).where("taggings.taggable_type" => "FakeModel") - assert authors.empty? + assert_empty authors authors = Author.joins(:similar_posts).where("taggings_authors_join.taggable_type" => "FakeModel") - assert authors.empty? + assert_empty authors end def test_nested_has_many_through_with_scope_on_polymorphic_reflection @@ -456,9 +456,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Ensure STI is respected in the join scope = Post.joins(:special_comments_ratings).where(id: posts(:sti_comments).id) - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? - assert !scope.where("comments.type" => "SubSpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") + assert_not_empty scope.where("comments.type" => "SubSpecialComment") end def test_has_many_through_with_sti_on_nested_through_reflection @@ -466,8 +466,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal [taggings(:special_comment_rating)], taggings scope = Post.joins(:special_comments_ratings_taggings).where(id: posts(:sti_comments).id) - assert scope.where("comments.type" => "Comment").empty? - assert !scope.where("comments.type" => "SpecialComment").empty? + assert_empty scope.where("comments.type" => "Comment") + assert_not_empty scope.where("comments.type" => "SpecialComment") end def test_nested_has_many_through_writers_should_raise_error @@ -517,7 +517,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_nested_has_many_through_with_conditions_on_through_associations_preload - assert Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags).empty? + assert_empty Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags) authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) } blue = tags(:blue) @@ -574,9 +574,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase c = Categorization.new c.author = authors(:david) c.post_taggings.to_a - assert !c.post_taggings.empty? + assert_not_empty c.post_taggings c.save - assert !c.post_taggings.empty? + assert_not_empty c.post_taggings end def test_polymorphic_has_many_through_when_through_association_has_not_loaded diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index de04221ea6..fce2a7eed1 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -47,7 +47,7 @@ class AssociationsTest < ActiveRecord::TestCase ship = Ship.create!(name: "The good ship Dollypop") part = ship.parts.create!(name: "Mast") part.mark_for_destruction - assert ship.parts[0].marked_for_destruction? + assert_predicate ship.parts[0], :marked_for_destruction? end def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction @@ -119,7 +119,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!")) - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? assert_includes david.posts, post end @@ -127,7 +127,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.categories << categories(:technology) - assert !david.categories.loaded? + assert_not_predicate david.categories, :loaded? assert_includes david.categories, categories(:technology) end @@ -135,23 +135,23 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!")) - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? david.save - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? assert_includes david.posts, post end def test_push_does_not_lose_additions_to_new_record josh = Author.new(name: "Josh") josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!") - assert josh.posts.loaded? + assert_predicate josh.posts, :loaded? assert_equal 1, josh.posts.size end def test_append_behaves_like_push josh = Author.new(name: "Josh") josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!") - assert josh.posts.loaded? + assert_predicate josh.posts, :loaded? assert_equal 1, josh.posts.size end @@ -163,22 +163,22 @@ class AssociationProxyTest < ActiveRecord::TestCase def test_save_on_parent_does_not_load_target david = developers(:david) - assert !david.projects.loaded? + assert_not_predicate david.projects, :loaded? david.update_columns(created_at: Time.now) - assert !david.projects.loaded? + assert_not_predicate david.projects, :loaded? end def test_load_does_load_target david = developers(:david) - assert !david.projects.loaded? + assert_not_predicate david.projects, :loaded? david.projects.load - assert david.projects.loaded? + assert_predicate david.projects, :loaded? end def test_inspect_does_not_reload_a_not_yet_loaded_target andreas = Developer.new name: "Andreas", log: "new developer added" - assert !andreas.audit_logs.loaded? + assert_not_predicate andreas.audit_logs, :loaded? assert_match(/message: "new developer added"/, andreas.audit_logs.inspect) end @@ -248,14 +248,14 @@ class AssociationProxyTest < ActiveRecord::TestCase test "first! works on loaded associations" do david = authors(:david) assert_equal david.first_posts.first, david.first_posts.reload.first! - assert david.first_posts.loaded? + assert_predicate david.first_posts, :loaded? assert_no_queries { david.first_posts.first! } end def test_pluck_uses_loaded_target david = authors(:david) assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title) - assert david.first_posts.loaded? + assert_predicate david.first_posts, :loaded? assert_no_queries { david.first_posts.pluck(:title) } end @@ -263,9 +263,9 @@ class AssociationProxyTest < ActiveRecord::TestCase david = authors(:david) david.posts.reload - assert david.posts.loaded? + assert_predicate david.posts, :loaded? david.posts.reset - assert !david.posts.loaded? + assert_not_predicate david.posts, :loaded? end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 2f42684212..dc6638d45d 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -99,8 +99,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase end test "boolean attributes" do - assert !Topic.find(1).approved? - assert Topic.find(2).approved? + assert_not_predicate Topic.find(1), :approved? + assert_predicate Topic.find(2), :approved? end test "set attributes" do @@ -142,16 +142,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_respond_to topic, :title= assert_respond_to topic, "author_name" assert_respond_to topic, "attribute_names" - assert !topic.respond_to?("nothingness") - assert !topic.respond_to?(:nothingness) + assert_not_respond_to topic, "nothingness" + assert_not_respond_to topic, :nothingness end test "respond_to? with a custom primary key" do keyboard = Keyboard.create assert_not_nil keyboard.key_number assert_equal keyboard.key_number, keyboard.id - assert keyboard.respond_to?("key_number") - assert keyboard.respond_to?("id") + assert_respond_to keyboard, "key_number" + assert_respond_to keyboard, "id" end test "id_before_type_cast with a custom primary key" do @@ -170,8 +170,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase end topic = klass.allocate - assert !topic.respond_to?("nothingness") - assert !topic.respond_to?(:nothingness) + assert_not_respond_to topic, "nothingness" + assert_not_respond_to topic, :nothingness assert_respond_to topic, "title" assert_respond_to topic, :title end @@ -200,12 +200,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase if current_adapter?(:Mysql2Adapter) test "read attributes_before_type_cast on a boolean" do bool = Boolean.create!("value" => false) - if RUBY_PLATFORM.include?("java") - # JRuby will return the value before typecast as string. - assert_equal "0", bool.reload.attributes_before_type_cast["value"] - else - assert_equal 0, bool.reload.attributes_before_type_cast["value"] - end + assert_equal 0, bool.reload.attributes_before_type_cast["value"] end end @@ -462,30 +457,30 @@ class AttributeMethodsTest < ActiveRecord::TestCase SQL assert_equal "Firm", object.string_value - assert object.string_value? + assert_predicate object, :string_value? object.string_value = " " - assert !object.string_value? + assert_not_predicate object, :string_value? assert_equal 1, object.int_value.to_i - assert object.int_value? + assert_predicate object, :int_value? object.int_value = "0" - assert !object.int_value? + assert_not_predicate object, :int_value? end test "non-attribute read and write" do topic = Topic.new - assert !topic.respond_to?("mumbo") + assert_not_respond_to topic, "mumbo" assert_raise(NoMethodError) { topic.mumbo } assert_raise(NoMethodError) { topic.mumbo = 5 } end test "undeclared attribute method does not affect respond_to? and method_missing" do topic = @target.new(title: "Budget") - assert topic.respond_to?("title") + assert_respond_to topic, "title" assert_equal "Budget", topic.title - assert !topic.respond_to?("title_hello_world") + assert_not_respond_to topic, "title_hello_world" assert_raise(NoMethodError) { topic.title_hello_world } end @@ -496,7 +491,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase @target.attribute_method_prefix prefix meth = "#{prefix}title" - assert topic.respond_to?(meth) + assert_respond_to topic, meth assert_equal ["title"], topic.send(meth) assert_equal ["title", "a"], topic.send(meth, "a") assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3) @@ -510,7 +505,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(title: "Budget") meth = "title#{suffix}" - assert topic.respond_to?(meth) + assert_respond_to topic, meth assert_equal ["title"], topic.send(meth) assert_equal ["title", "a"], topic.send(meth, "a") assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3) @@ -524,7 +519,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(title: "Budget") meth = "#{prefix}title#{suffix}" - assert topic.respond_to?(meth) + assert_respond_to topic, meth assert_equal ["title"], topic.send(meth) assert_equal ["title", "a"], topic.send(meth, "a") assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3) @@ -546,7 +541,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase else topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first end - assert !topic.is_test? + assert_not_predicate topic, :is_test? end test "typecast attribute from select to true" do @@ -557,7 +552,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase else topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first end - assert topic.is_test? + assert_predicate topic, :is_test? end test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do @@ -748,7 +743,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase expected_time = Time.utc(2000, 01, 01, 10) assert_equal expected_time, record.bonus_time - assert record.bonus_time.utc? + assert_predicate record.bonus_time, :utc? end end end @@ -772,7 +767,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase privatize("title") topic = @target.new(title: "The pros and cons of programming naked.") - assert !topic.respond_to?(:title) + assert_not_respond_to topic, :title exception = assert_raise(NoMethodError) { topic.title } assert_includes exception.message, "private method" assert_equal "I'm private", topic.send(:title) @@ -782,7 +777,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase privatize("title=(value)") topic = @target.new - assert !topic.respond_to?(:title=) + assert_not_respond_to topic, :title= exception = assert_raise(NoMethodError) { topic.title = "Pants" } assert_includes exception.message, "private method" topic.send(:title=, "Very large pants") @@ -792,7 +787,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase privatize("title?") topic = @target.new(title: "Isaac Newton's pants") - assert !topic.respond_to?(:title?) + assert_not_respond_to topic, :title? exception = assert_raise(NoMethodError) { topic.title? } assert_includes exception.message, "private method" assert topic.send(:title?) @@ -984,9 +979,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "came_from_user?" do model = @target.first - assert_not model.id_came_from_user? + assert_not_predicate model, :id_came_from_user? model.id = "omg" - assert model.id_came_from_user? + assert_predicate model, :id_came_from_user? end test "accessed_fields" do diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 2caf2a63d4..3bc56694be 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -59,7 +59,7 @@ module ActiveRecord test "nonexistent attribute" do data = OverloadedType.new(non_existent_decimal: 1) - assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_equal BigDecimal(1), data.non_existent_decimal assert_raise ActiveRecord::UnknownAttributeError do UnoverloadedType.new(non_existent_decimal: 1) end @@ -211,7 +211,7 @@ module ActiveRecord end test "attributes not backed by database columns are not dirty when unchanged" do - refute OverloadedType.new.non_existent_decimal_changed? + assert_not_predicate OverloadedType.new, :non_existent_decimal_changed? end test "attributes not backed by database columns are always initialized" do @@ -245,13 +245,13 @@ module ActiveRecord model.foo << "asdf" assert_equal "lolasdf", model.foo - assert model.foo_changed? + assert_predicate model, :foo_changed? model.reload assert_equal "lol", model.foo model.foo = "lol" - refute model.changed? + assert_not_predicate model, :changed? end test "attributes not backed by database columns appear in inspect" do diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 4d8368fd8a..09684520d1 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -51,7 +51,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase u = person.create!(first_name: "cool") u.update_attributes!(first_name: "nah") # still valid because validation only applies on 'create' - assert reference.create!(person: u).persisted? + assert_predicate reference.create!(person: u), :persisted? end def test_should_not_add_the_same_callbacks_multiple_times_for_has_one @@ -74,7 +74,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase ship = ShipWithoutNestedAttributes.new ship.prisoners.build - assert_not ship.valid? + assert_not_predicate ship, :valid? assert_equal 1, ship.errors[:name].length end @@ -99,35 +99,35 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas def test_should_save_parent_but_not_invalid_child firm = Firm.new(name: "GlobalMegaCorp") - assert firm.valid? + assert_predicate firm, :valid? firm.build_account_using_primary_key - assert !firm.build_account_using_primary_key.valid? + assert_not_predicate firm.build_account_using_primary_key, :valid? assert firm.save - assert !firm.account_using_primary_key.persisted? + assert_not_predicate firm.account_using_primary_key, :persisted? end def test_save_fails_for_invalid_has_one firm = Firm.first - assert firm.valid? + assert_predicate firm, :valid? firm.build_account - assert !firm.account.valid? - assert !firm.valid? + assert_not_predicate firm.account, :valid? + assert_not_predicate firm, :valid? assert !firm.save assert_equal ["is invalid"], firm.errors["account"] end def test_save_succeeds_for_invalid_has_one_with_validate_false firm = Firm.first - assert firm.valid? + assert_predicate firm, :valid? firm.build_unvalidated_account - assert !firm.unvalidated_account.valid? - assert firm.valid? + assert_not_predicate firm.unvalidated_account, :valid? + assert_predicate firm, :valid? assert firm.save end @@ -136,10 +136,10 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas account = firm.build_account("credit_limit" => 1000) assert_equal account, firm.account - assert !account.persisted? + assert_not_predicate account, :persisted? assert firm.save assert_equal account, firm.account - assert account.persisted? + assert_predicate account, :persisted? end def test_build_before_either_saved @@ -147,16 +147,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm.account = account = Account.new("credit_limit" => 1000) assert_equal account, firm.account - assert !account.persisted? + assert_not_predicate account, :persisted? assert firm.save assert_equal account, firm.account - assert account.persisted? + assert_predicate account, :persisted? end def test_assignment_before_parent_saved firm = Firm.new("name" => "GlobalMegaCorp") firm.account = a = Account.find(1) - assert !firm.persisted? + assert_not_predicate firm, :persisted? assert_equal a, firm.account assert firm.save assert_equal a, firm.account @@ -167,12 +167,12 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas def test_assignment_before_either_saved firm = Firm.new("name" => "GlobalMegaCorp") firm.account = a = Account.new("credit_limit" => 1000) - assert !firm.persisted? - assert !a.persisted? + assert_not_predicate firm, :persisted? + assert_not_predicate a, :persisted? assert_equal a, firm.account assert firm.save - assert firm.persisted? - assert a.persisted? + assert_predicate firm, :persisted? + assert_predicate a, :persisted? assert_equal a, firm.account firm.association(:account).reload assert_equal a, firm.account @@ -221,13 +221,13 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test def test_should_save_parent_but_not_invalid_child client = Client.new(name: "Joe (the Plumber)") - assert client.valid? + assert_predicate client, :valid? client.build_firm - assert !client.firm.valid? + assert_not_predicate client.firm, :valid? assert client.save - assert !client.firm.persisted? + assert_not_predicate client.firm, :persisted? end def test_save_fails_for_invalid_belongs_to @@ -235,8 +235,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert log = AuditLog.create(developer_id: 0, message: " ") log.developer = Developer.new - assert !log.developer.valid? - assert !log.valid? + assert_not_predicate log.developer, :valid? + assert_not_predicate log, :valid? assert !log.save assert_equal ["is invalid"], log.errors["developer"] end @@ -246,8 +246,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert log = AuditLog.create(developer_id: 0, message: " ") log.unvalidated_developer = Developer.new - assert !log.unvalidated_developer.valid? - assert log.valid? + assert_not_predicate log.unvalidated_developer, :valid? + assert_predicate log, :valid? assert log.save end @@ -256,10 +256,10 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test apple = Firm.new("name" => "Apple") client.firm = apple assert_equal apple, client.firm - assert !apple.persisted? + assert_not_predicate apple, :persisted? assert client.save assert apple.save - assert apple.persisted? + assert_predicate apple, :persisted? assert_equal apple, client.firm client.association(:firm).reload assert_equal apple, client.firm @@ -269,11 +269,11 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test final_cut = Client.new("name" => "Final Cut") apple = Firm.new("name" => "Apple") final_cut.firm = apple - assert !final_cut.persisted? - assert !apple.persisted? + assert_not_predicate final_cut, :persisted? + assert_not_predicate apple, :persisted? assert final_cut.save - assert final_cut.persisted? - assert apple.persisted? + assert_predicate final_cut, :persisted? + assert_predicate apple, :persisted? assert_equal apple, final_cut.firm final_cut.association(:firm).reload assert_equal apple, final_cut.firm @@ -382,7 +382,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test auditlog.developer = invalid_developer auditlog.developer_id = valid_developer.id - assert auditlog.valid? + assert_predicate auditlog, :valid? end end @@ -395,8 +395,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] molecule.save - assert_not invalid_electron.valid? - assert valid_electron.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? assert_not molecule.persisted?, "Molecule should not be persisted when its electrons are invalid" end @@ -408,9 +408,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] - assert_not tuning_peg_invalid.valid? - assert tuning_peg_valid.valid? - assert_not guitar.valid? + assert_not_predicate tuning_peg_invalid, :valid? + assert_predicate tuning_peg_valid, :valid? + assert_not_predicate guitar, :valid? assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"] assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"] end @@ -425,9 +425,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] - assert_not invalid_electron.valid? - assert valid_electron.valid? - assert_not molecule.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? + assert_not_predicate molecule, :valid? assert_equal ["can't be blank"], molecule.errors["electrons[1].name"] assert_not_equal ["can't be blank"], molecule.errors["electrons.name"] ensure @@ -441,9 +441,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] - assert_not invalid_electron.valid? - assert valid_electron.valid? - assert_not molecule.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? + assert_not_predicate molecule, :valid? assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"] end @@ -455,9 +455,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] - assert_not tuning_peg_invalid.valid? - assert tuning_peg_valid.valid? - assert_not guitar.valid? + assert_not_predicate tuning_peg_invalid, :valid? + assert_predicate tuning_peg_valid, :valid? + assert_not_predicate guitar, :valid? assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"] assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"] end @@ -472,9 +472,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron, invalid_electron] - assert_not invalid_electron.valid? - assert valid_electron.valid? - assert_not molecule.valid? + assert_not_predicate invalid_electron, :valid? + assert_predicate valid_electron, :valid? + assert_not_predicate molecule, :valid? assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"] assert_equal [], molecule.errors.details[:"electrons.name"] ensure @@ -488,8 +488,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib molecule.electrons = [valid_electron] molecule.save - assert valid_electron.valid? - assert molecule.persisted? + assert_predicate valid_electron, :valid? + assert_predicate molecule, :persisted? assert_equal 1, molecule.electrons.count end end @@ -500,21 +500,21 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_invalid_adding firm = Firm.find(1) assert !(firm.clients_of_firm << c = Client.new) - assert !c.persisted? - assert !firm.valid? + assert_not_predicate c, :persisted? + assert_not_predicate firm, :valid? assert !firm.save - assert !c.persisted? + assert_not_predicate c, :persisted? end def test_invalid_adding_before_save new_firm = Firm.new("name" => "A New Firm, Inc") new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")]) - assert !c.persisted? - assert !c.valid? - assert !new_firm.valid? + assert_not_predicate c, :persisted? + assert_not_predicate c, :valid? + assert_not_predicate new_firm, :valid? assert !new_firm.save - assert !c.persisted? - assert !new_firm.persisted? + assert_not_predicate c, :persisted? + assert_not_predicate new_firm, :persisted? end def test_invalid_adding_with_validate_false @@ -522,10 +522,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa client = Client.new firm.unvalidated_clients_of_firm << client - assert firm.valid? - assert !client.valid? + assert_predicate firm, :valid? + assert_not_predicate client, :valid? assert firm.save - assert !client.persisted? + assert_not_predicate client, :persisted? end def test_valid_adding_with_validate_false @@ -534,24 +534,24 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa firm = Firm.first client = Client.new("name" => "Apple") - assert firm.valid? - assert client.valid? - assert !client.persisted? + assert_predicate firm, :valid? + assert_predicate client, :valid? + assert_not_predicate client, :persisted? firm.unvalidated_clients_of_firm << client assert firm.save - assert client.persisted? + assert_predicate client, :persisted? assert_equal no_of_clients + 1, Client.count end def test_invalid_build new_client = companies(:first_firm).clients_of_firm.build - assert !new_client.persisted? - assert !new_client.valid? + assert_not_predicate new_client, :persisted? + assert_not_predicate new_client, :valid? assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save - assert !new_client.persisted? + assert_not_predicate new_client, :persisted? assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end @@ -570,8 +570,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_firms, Firm.count # Firm was not saved to database. assert_equal no_of_clients, Client.count # Clients were not saved to database. assert new_firm.save - assert new_firm.persisted? - assert c.persisted? + assert_predicate new_firm, :persisted? + assert_predicate c, :persisted? assert_equal new_firm, c.firm assert_equal no_of_firms + 1, Firm.count # Firm was saved to database. assert_equal no_of_clients + 2, Client.count # Clients were saved to database. @@ -601,11 +601,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") } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" assert_queries(2) { assert company.save } - assert new_client.persisted? + assert_predicate new_client, :persisted? assert_equal 3, company.clients_of_firm.reload.size end @@ -621,11 +621,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" } } - assert !company.clients_of_firm.loaded? + assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" assert_queries(2) { assert company.save } - assert new_client.persisted? + assert_predicate new_client, :persisted? assert_equal 3, company.clients_of_firm.reload.size end @@ -657,62 +657,62 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase new_account = Account.new("credit_limit" => 1000) new_firm = Firm.new("name" => "some firm") - assert !new_firm.persisted? + assert_not_predicate new_firm, :persisted? new_account.firm = new_firm new_account.save! - assert new_firm.persisted? + assert_predicate new_firm, :persisted? new_account = Account.new("credit_limit" => 1000) new_autosaved_firm = Firm.new("name" => "some firm") - assert !new_autosaved_firm.persisted? + assert_not_predicate new_autosaved_firm, :persisted? new_account.unautosaved_firm = new_autosaved_firm new_account.save! - assert !new_autosaved_firm.persisted? + assert_not_predicate new_autosaved_firm, :persisted? end def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert !account.persisted? + assert_not_predicate account, :persisted? firm.account = account firm.save! - assert account.persisted? + assert_predicate account, :persisted? firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) firm.unautosaved_account = account - assert !account.persisted? + assert_not_predicate account, :persisted? firm.unautosaved_account = account firm.save! - assert !account.persisted? + assert_not_predicate account, :persisted? end def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert !account.persisted? + assert_not_predicate account, :persisted? firm.accounts << account firm.save! - assert account.persisted? + assert_predicate account, :persisted? firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert !account.persisted? + assert_not_predicate account, :persisted? firm.unautosaved_accounts << account firm.save! - assert !account.persisted? + assert_not_predicate account, :persisted? end def test_autosave_new_record_with_after_create_callback @@ -745,18 +745,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.mark_for_destruction @pirate.ship.mark_for_destruction - assert !@pirate.reload.marked_for_destruction? - assert !@pirate.ship.reload.marked_for_destruction? + assert_not_predicate @pirate.reload, :marked_for_destruction? + assert_not_predicate @pirate.ship.reload, :marked_for_destruction? end # has_one def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction - assert !@pirate.ship.marked_for_destruction? + assert_not_predicate @pirate.ship, :marked_for_destruction? @pirate.ship.mark_for_destruction id = @pirate.ship.id - assert @pirate.ship.marked_for_destruction? + assert_predicate @pirate.ship, :marked_for_destruction? assert Ship.find_by_id(id) @pirate.save @@ -766,7 +766,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_skip_validation_on_a_child_association_if_marked_for_destruction @pirate.ship.name = "" - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.ship.mark_for_destruction @pirate.ship.expects(:valid?).never @@ -812,12 +812,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction - assert !@ship.pirate.marked_for_destruction? + assert_not_predicate @ship.pirate, :marked_for_destruction? @ship.pirate.mark_for_destruction id = @ship.pirate.id - assert @ship.pirate.marked_for_destruction? + assert_predicate @ship.pirate, :marked_for_destruction? assert Pirate.find_by_id(id) @ship.save @@ -827,7 +827,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction @ship.pirate.catchphrase = "" - assert !@ship.valid? + assert_not_predicate @ship, :valid? @ship.pirate.mark_for_destruction @ship.pirate.expects(:valid?).never @@ -881,7 +881,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase ids.each { |id| assert klass.find_by_id(id) } @pirate.save - assert @pirate.reload.birds.empty? + assert_empty @pirate.reload.birds ids.each { |id| assert_nil klass.find_by_id(id) } end @@ -889,14 +889,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.birds.create!(name: :parrot) @pirate.birds.first.destroy @pirate.save! - assert @pirate.reload.birds.empty? + assert_empty @pirate.reload.birds end def test_should_skip_validation_on_has_many_if_marked_for_destruction 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") } @pirate.birds.each { |bird| bird.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.birds.each do |bird| bird.mark_for_destruction @@ -909,10 +909,10 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.birds.create!(name: "birds_1") @pirate.birds.each { |bird| bird.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.birds.each(&:destroy) - assert @pirate.valid? + assert_predicate @pirate, :valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many @@ -1010,17 +1010,17 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.save end - assert @pirate.reload.parrots.empty? + assert_empty @pirate.reload.parrots join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}") - assert join_records.empty? + assert_empty join_records end def test_should_skip_validation_on_habtm_if_marked_for_destruction 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") } @pirate.parrots.each { |parrot| parrot.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.parrots.each do |parrot| parrot.mark_for_destruction @@ -1028,17 +1028,17 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end @pirate.save! - assert @pirate.reload.parrots.empty? + assert_empty @pirate.reload.parrots end def test_should_skip_validation_on_habtm_if_destroyed @pirate.parrots.create!(name: "parrots_1") @pirate.parrots.each { |parrot| parrot.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? @pirate.parrots.each(&:destroy) - assert @pirate.valid? + assert_predicate @pirate, :valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm @@ -1145,16 +1145,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @pirate.ship.name = "" - assert @pirate.invalid? - assert @pirate.errors[:"ship.name"].any? + assert_predicate @pirate, :invalid? + assert_predicate @pirate.errors[:"ship.name"], :any? end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid @pirate.ship.name = nil @pirate.catchphrase = nil - assert @pirate.invalid? - assert @pirate.errors[:"ship.name"].any? - assert @pirate.errors[:catchphrase].any? + assert_predicate @pirate, :invalid? + assert_predicate @pirate.errors[:"ship.name"], :any? + assert_predicate @pirate.errors[:catchphrase], :any? end def test_should_not_ignore_different_error_messages_on_the_same_attribute @@ -1163,7 +1163,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase Ship.validates_format_of :name, with: /\w/ @pirate.ship.name = "" @pirate.catchphrase = nil - assert @pirate.invalid? + assert_predicate @pirate, :invalid? assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"] ensure Ship._validators = old_validators if old_validators @@ -1244,7 +1244,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase ship = ShipWithoutNestedAttributes.new(name: "The Black Flag") ship.parts.build.mark_for_destruction - assert_not ship.valid? + assert_not_predicate ship, :valid? end end @@ -1299,16 +1299,16 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @ship.pirate.catchphrase = "" - assert @ship.invalid? - assert @ship.errors[:"pirate.catchphrase"].any? + assert_predicate @ship, :invalid? + assert_predicate @ship.errors[:"pirate.catchphrase"], :any? end def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid @ship.name = nil @ship.pirate.catchphrase = nil - assert @ship.invalid? - assert @ship.errors[:name].any? - assert @ship.errors[:"pirate.catchphrase"].any? + assert_predicate @ship, :invalid? + assert_predicate @ship.errors[:name], :any? + assert_predicate @ship.errors[:"pirate.catchphrase"], :any? end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -1403,17 +1403,17 @@ module AutosaveAssociationOnACollectionAssociationTests def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"] - assert @pirate.errors[@association_name].empty? + assert_empty @pirate.errors[@association_name] end def test_should_not_use_default_invalid_error_on_associated_models @pirate.send(@association_name).build(name: "") - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"] - assert @pirate.errors[@association_name].empty? + assert_empty @pirate.errors[@association_name] end def test_should_default_invalid_error_from_i18n @@ -1423,10 +1423,10 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).build(name: "") - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"] assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages - assert @pirate.errors[@association_name].empty? + assert_empty @pirate.errors[@association_name] ensure I18n.backend = I18n::Backend::Simple.new end @@ -1435,9 +1435,9 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each { |child| child.name = "" } @pirate.catchphrase = nil - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"] - assert @pirate.errors[:catchphrase].any? + assert_predicate @pirate.errors[:catchphrase], :any? end def test_should_allow_to_bypass_validations_on_the_associated_models_on_update @@ -1595,10 +1595,10 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te end test "should automatically validate associations" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.birds.each { |bird| bird.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end end @@ -1613,15 +1613,15 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes end test "should automatically validate associations with :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.ship.name = "" - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end test "should not automatically add validate associations without :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.non_validated_ship.name = "" - assert @pirate.valid? + assert_predicate @pirate, :valid? end end @@ -1634,15 +1634,15 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord:: end test "should automatically validate associations with :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.parrot = Parrot.new(name: "") - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end test "should not automatically validate associations without :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.non_validated_parrot = Parrot.new(name: "") - assert @pirate.valid? + assert_predicate @pirate, :valid? end end @@ -1655,17 +1655,17 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test end test "should automatically validate associations with :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.parrots = [ Parrot.new(name: "popuga") ] @pirate.parrots.each { |parrot| parrot.name = "" } - assert !@pirate.valid? + assert_not_predicate @pirate, :valid? end test "should not automatically validate associations without :validate => true" do - assert @pirate.valid? + assert_predicate @pirate, :valid? @pirate.non_validated_parrots = [ Parrot.new(name: "popuga") ] @pirate.non_validated_parrots.each { |parrot| parrot.name = "" } - assert @pirate.valid? + assert_predicate @pirate, :valid? end end @@ -1686,7 +1686,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas end test "should not generate validation methods for has_one associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_ship) + assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_ship end test "should generate validation methods for belongs_to associations with :validate => true" do @@ -1694,7 +1694,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas end test "should not generate validation methods for belongs_to associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrot) + assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_parrot end test "should generate validation methods for HABTM associations with :validate => true" do diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index f0ef522515..983a3d366a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -104,7 +104,7 @@ class BasicsTest < ActiveRecord::TestCase pk = Author.columns_hash["id"] ref = Post.columns_hash["author_id"] - assert_equal pk.bigint?, ref.bigint? + assert_equal pk.sql_type, ref.sql_type end def test_many_mutations @@ -147,8 +147,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_table_exists - assert !NonExistentTable.table_exists? - assert Topic.table_exists? + assert_not_predicate NonExistentTable, :table_exists? + assert_predicate Topic, :table_exists? end def test_preserving_date_objects @@ -450,7 +450,7 @@ class BasicsTest < ActiveRecord::TestCase def test_default_values topic = Topic.new - assert topic.approved? + assert_predicate topic, :approved? assert_nil topic.written_on assert_nil topic.bonus_time assert_nil topic.last_read @@ -458,7 +458,7 @@ class BasicsTest < ActiveRecord::TestCase topic.save topic = Topic.find(topic.id) - assert topic.approved? + assert_predicate topic, :approved? assert_nil topic.last_read # Oracle has some funky default handling, so it requires a bit of @@ -629,7 +629,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_readonly_attributes - assert_equal Set.new([ "title" , "comments_count" ]), ReadonlyTitlePost.readonly_attributes + assert_equal Set.new([ "title", "comments_count" ]), ReadonlyTitlePost.readonly_attributes post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable") post.reload @@ -727,9 +727,9 @@ class BasicsTest < ActiveRecord::TestCase b_nil = Boolean.find(nil_id) assert_nil b_nil.value b_false = Boolean.find(false_id) - assert !b_false.value? + assert_not_predicate b_false, :value? b_true = Boolean.find(true_id) - assert b_true.value? + assert_predicate b_true, :value? end def test_boolean_without_questionmark @@ -753,9 +753,9 @@ class BasicsTest < ActiveRecord::TestCase b_blank = Boolean.find(blank_id) assert_nil b_blank.value b_false = Boolean.find(false_id) - assert !b_false.value? + assert_not_predicate b_false, :value? b_true = Boolean.find(true_id) - assert b_true.value? + assert_predicate b_true, :value? end def test_new_record_returns_boolean @@ -768,7 +768,7 @@ class BasicsTest < ActiveRecord::TestCase duped_topic = nil assert_nothing_raised { duped_topic = topic.dup } assert_equal topic.title, duped_topic.title - assert !duped_topic.persisted? + assert_not_predicate duped_topic, :persisted? # test if the attributes have been duped topic.title = "a" @@ -786,7 +786,7 @@ class BasicsTest < ActiveRecord::TestCase # test if saved clone object differs from original duped_topic.save - assert duped_topic.persisted? + assert_predicate duped_topic, :persisted? assert_not_equal duped_topic.id, topic.id duped_topic.reload @@ -807,7 +807,7 @@ class BasicsTest < ActiveRecord::TestCase assert_nothing_raised { dup = dev.dup } assert_kind_of DeveloperSalary, dup.salary assert_equal dev.salary.amount, dup.salary.amount - assert !dup.persisted? + assert_not_predicate dup, :persisted? # test if the attributes have been duped original_amount = dup.salary.amount @@ -815,7 +815,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal original_amount, dup.salary.amount assert dup.save - assert dup.persisted? + assert_predicate dup, :persisted? assert_not_equal dup.id, dev.id end @@ -835,22 +835,22 @@ class BasicsTest < ActiveRecord::TestCase def test_clone_of_new_object_with_defaults developer = Developer.new - assert !developer.name_changed? - assert !developer.salary_changed? + assert_not_predicate developer, :name_changed? + assert_not_predicate developer, :salary_changed? cloned_developer = developer.clone - assert !cloned_developer.name_changed? - assert !cloned_developer.salary_changed? + assert_not_predicate cloned_developer, :name_changed? + assert_not_predicate cloned_developer, :salary_changed? end def test_clone_of_new_object_marks_attributes_as_dirty developer = Developer.new name: "Bjorn", salary: 100000 - assert developer.name_changed? - assert developer.salary_changed? + assert_predicate developer, :name_changed? + assert_predicate developer, :salary_changed? cloned_developer = developer.clone - assert cloned_developer.name_changed? - assert cloned_developer.salary_changed? + assert_predicate cloned_developer, :name_changed? + assert_predicate cloned_developer, :salary_changed? end def test_clone_of_new_object_marks_as_dirty_only_changed_attributes @@ -859,24 +859,24 @@ class BasicsTest < ActiveRecord::TestCase assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed cloned_developer = developer.clone - assert cloned_developer.name_changed? + assert_predicate cloned_developer, :name_changed? assert !cloned_developer.salary_changed? # ... and cloned instance should behave same end def test_dup_of_saved_object_marks_attributes_as_dirty developer = Developer.create! name: "Bjorn", salary: 100000 - assert !developer.name_changed? - assert !developer.salary_changed? + assert_not_predicate developer, :name_changed? + assert_not_predicate developer, :salary_changed? cloned_developer = developer.dup assert cloned_developer.name_changed? # both attributes differ from defaults - assert cloned_developer.salary_changed? + assert_predicate cloned_developer, :salary_changed? end def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes developer = Developer.create! name: "Bjorn" assert !developer.name_changed? # both attributes of saved object should be treated as not changed - assert !developer.salary_changed? + assert_not_predicate developer, :salary_changed? cloned_developer = developer.dup assert cloned_developer.name_changed? # ... but on cloned object should be @@ -891,11 +891,9 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 2147483648, company.rating end - unless current_adapter?(:SQLite3Adapter) - def test_bignum_pk - company = Company.create!(id: 2147483648, name: "foo") - assert_equal company, Company.find(company.id) - end + def test_bignum_pk + company = Company.create!(id: 2147483648, name: "foo") + assert_equal company, Company.find(company.id) end # TODO: extend defaults tests to other databases! @@ -953,14 +951,14 @@ class BasicsTest < ActiveRecord::TestCase end def test_toggle_attribute - assert !topics(:first).approved? + assert_not_predicate topics(:first), :approved? topics(:first).toggle!(:approved) - assert topics(:first).approved? + assert_predicate topics(:first), :approved? topic = topics(:first) topic.toggle(:approved) - assert !topic.approved? + assert_not_predicate topic, :approved? topic.reload - assert topic.approved? + assert_predicate topic, :approved? end def test_reload @@ -1433,11 +1431,11 @@ class BasicsTest < ActiveRecord::TestCase test "resetting column information doesn't remove attribute methods" do topic = topics(:first) - assert_not topic.id_changed? + assert_not_predicate topic, :id_changed? Topic.reset_column_information - assert_not topic.id_changed? + assert_not_predicate topic, :id_changed? end test "ignored columns are not present in columns_hash" do @@ -1449,31 +1447,76 @@ class BasicsTest < ActiveRecord::TestCase end test "ignored columns have no attribute methods" do - refute Developer.new.respond_to?(:first_name) - refute Developer.new.respond_to?(:first_name=) - refute Developer.new.respond_to?(:first_name?) - refute SubDeveloper.new.respond_to?(:first_name) - refute SubDeveloper.new.respond_to?(:first_name=) - refute SubDeveloper.new.respond_to?(:first_name?) - refute SymbolIgnoredDeveloper.new.respond_to?(:first_name) - refute SymbolIgnoredDeveloper.new.respond_to?(:first_name=) - refute SymbolIgnoredDeveloper.new.respond_to?(:first_name?) + assert_not_respond_to Developer.new, :first_name + assert_not_respond_to Developer.new, :first_name= + assert_not_respond_to Developer.new, :first_name? + assert_not_respond_to SubDeveloper.new, :first_name + assert_not_respond_to SubDeveloper.new, :first_name= + assert_not_respond_to SubDeveloper.new, :first_name? + assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name + assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name= + assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name? end test "ignored columns don't prevent explicit declaration of attribute methods" do - assert Developer.new.respond_to?(:last_name) - assert Developer.new.respond_to?(:last_name=) - assert Developer.new.respond_to?(:last_name?) - assert SubDeveloper.new.respond_to?(:last_name) - assert SubDeveloper.new.respond_to?(:last_name=) - assert SubDeveloper.new.respond_to?(:last_name?) - assert SymbolIgnoredDeveloper.new.respond_to?(:last_name) - assert SymbolIgnoredDeveloper.new.respond_to?(:last_name=) - assert SymbolIgnoredDeveloper.new.respond_to?(:last_name?) + assert_respond_to Developer.new, :last_name + assert_respond_to Developer.new, :last_name= + assert_respond_to Developer.new, :last_name? + assert_respond_to SubDeveloper.new, :last_name + assert_respond_to SubDeveloper.new, :last_name= + assert_respond_to SubDeveloper.new, :last_name? + assert_respond_to SymbolIgnoredDeveloper.new, :last_name + assert_respond_to SymbolIgnoredDeveloper.new, :last_name= + assert_respond_to SymbolIgnoredDeveloper.new, :last_name? end test "ignored columns are stored as an array of string" do assert_equal(%w(first_name last_name), Developer.ignored_columns) assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns) end + + test "when #reload called, ignored columns' attribute methods are not defined" do + developer = Developer.create!(name: "Developer") + assert_not_respond_to developer, :first_name + assert_not_respond_to developer, :first_name= + + developer.reload + + assert_not_respond_to developer, :first_name + assert_not_respond_to developer, :first_name= + end + + test "ignored columns not included in SELECT" do + query = Developer.all.to_sql.downcase + + # ignored column + assert_not query.include?("first_name") + + # regular column + assert query.include?("name") + end + + test "column names are quoted when using #from clause and model has ignored columns" do + refute_empty Developer.ignored_columns + query = Developer.from("developers").to_sql + quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" + + assert_match(/SELECT #{quoted_id}.* FROM developers/, query) + end + + test "using table name qualified column names unless having SELECT list explicitly" do + assert_equal developers(:david), Developer.from("developers").joins(:shared_computers).take + end + + test "protected environments by default is an array with production" do + assert_equal ["production"], ActiveRecord::Base.protected_environments + end + + def test_protected_environments_are_stored_as_an_array_of_string + previous_protected_environments = ActiveRecord::Base.protected_environments + ActiveRecord::Base.protected_environments = [:staging, "production"] + assert_equal ["staging", "production"], ActiveRecord::Base.protected_environments + ensure + ActiveRecord::Base.protected_environments = previous_protected_environments + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index be8aeed5ac..ad701fb200 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -313,7 +313,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_each_record_should_yield_record_if_block_is_given assert_queries(6) do Post.in_batches(of: 2).each_record do |post| - assert post.title.present? + assert_predicate post.title, :present? assert_kind_of Post, post end end @@ -322,7 +322,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_each_record_should_return_enumerator_if_no_block_given assert_queries(6) do Post.in_batches(of: 2).each_record.with_index do |post, i| - assert post.title.present? + assert_predicate post.title, :present? assert_kind_of Post, post end end @@ -353,24 +353,24 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_not_be_loaded Post.in_batches(of: 1) do |relation| - assert_not relation.loaded? + assert_not_predicate relation, :loaded? end Post.in_batches(of: 1, load: false) do |relation| - assert_not relation.loaded? + assert_not_predicate relation, :loaded? end end def test_in_batches_should_be_loaded Post.in_batches(of: 1, load: true) do |relation| - assert relation.loaded? + assert_predicate relation, :loaded? end end def test_in_batches_if_not_loaded_executes_more_queries assert_queries(@total + 1) do Post.in_batches(of: 1, load: false) do |relation| - assert_not relation.loaded? + assert_not_predicate relation, :loaded? end end end @@ -592,7 +592,11 @@ class EachTest < ActiveRecord::TestCase table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - posts = ActiveRecord::Relation.create(Post, table_alias, predicate_builder) + posts = ActiveRecord::Relation.create( + Post, + table: table_alias, + predicate_builder: predicate_builder + ) posts.find_each {} end end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 8f2f2c6186..3a569f226e 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -38,8 +38,8 @@ module ActiveRecord end test "cache_version is only there when versioning is on" do - assert CacheMeWithVersion.create.cache_version.present? - assert_not CacheMe.create.cache_version.present? + assert_predicate CacheMeWithVersion.create.cache_version, :present? + assert_not_predicate CacheMe.create.cache_version, :present? end test "cache_key_with_version always has both key and version" do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 66bc14b5ab..ec8bc913a1 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -21,7 +21,7 @@ require "models/comment" require "models/rating" class CalculationsTest < ActiveRecord::TestCase - fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books + fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments def test_should_sum_field assert_equal 318, Account.sum(:credit_limit) @@ -236,6 +236,18 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_count_with_eager_loading_and_custom_order + posts = Post.includes(:comments).order("comments.id") + assert_queries(1) { assert_equal 11, posts.count } + assert_queries(1) { assert_equal 11, posts.count(:all) } + end + + def test_distinct_count_all_with_custom_select_and_order + accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10")) + assert_queries(1) { assert_equal 3, accounts.count(:all) } + assert_queries(1) { assert_equal 3, accounts.load.size } + end + def test_distinct_count_with_order_and_limit assert_equal 4, Account.distinct.order(:firm_id).limit(4).count end @@ -663,14 +675,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_with_selection_clause - assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT credit_limit").sort - assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT accounts.credit_limit").sort - assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT(credit_limit)").sort + assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT credit_limit")).sort + assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT accounts.credit_limit")).sort + assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT(credit_limit)")).sort # MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless # an alias is provided. Without the alias, the column cannot be found # and properly typecast. - assert_equal [50 + 53 + 55 + 60], Account.pluck("SUM(DISTINCT(credit_limit)) as credit_limit") + assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit)) as credit_limit")) end def test_plucks_with_ids @@ -682,6 +694,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id) end + def test_pluck_with_includes_offset + assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id) + assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id) + end + def test_pluck_not_auto_table_name_prefix_if_column_included Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)]) ids = Company.includes(:contracts).pluck(:developer_id) @@ -772,10 +789,20 @@ class CalculationsTest < ActiveRecord::TestCase companies = Company.order(:name).limit(3).load assert_queries 1 do - assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck("DISTINCT name") + assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name")) end end + def test_pick_one + assert_equal "The First Topic", Topic.order(:id).pick(:heading) + assert_nil Topic.where("1=0").pick(:heading) + end + + def test_pick_two + assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address) + assert_nil Topic.where("1=0").pick(:author_name, :author_email_address) + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 55c7475f46..3b283a3aa6 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -394,27 +394,27 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_create_throwing_abort someone = CallbackHaltedDeveloper.new someone.cancel_before_create = true - assert someone.valid? + assert_predicate someone, :valid? assert !someone.save assert_save_callbacks_not_called(someone) end def test_before_save_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) - assert david.valid? + assert_predicate david, :valid? assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 - assert !david.valid? + assert_not_predicate david, :valid? assert !david.save assert_raise(ActiveRecord::RecordInvalid) { david.save! } someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_save = true - assert someone.valid? + assert_predicate someone, :valid? assert !someone.save assert_save_callbacks_not_called(someone) end @@ -422,7 +422,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_update_throwing_abort someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_update = true - assert someone.valid? + assert_predicate someone, :valid? assert !someone.save assert_save_callbacks_not_called(someone) end diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb index 3187e6aed5..65e5016040 100644 --- a/activerecord/test/cases/clone_test.rb +++ b/activerecord/test/cases/clone_test.rb @@ -36,7 +36,7 @@ module ActiveRecord cloned = Topic.new clone = cloned.clone cloned.freeze - assert_not clone.frozen? + assert_not_predicate clone, :frozen? end end end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 19d6464a22..a5d908344a 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -24,7 +24,7 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end @@ -37,7 +37,7 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end @@ -50,7 +50,7 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end @@ -60,7 +60,11 @@ module ActiveRecord table_metadata = ActiveRecord::TableMetadata.new(Developer, table_alias) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - developers = ActiveRecord::Relation.create(Developer, table_alias, predicate_builder) + developers = ActiveRecord::Relation.create( + Developer, + table: table_alias, + predicate_builder: predicate_builder + ) developers = developers.where(salary: 100000).order(updated_at: :desc) last_developer_timestamp = developers.first.updated_at @@ -68,7 +72,7 @@ module ActiveRecord /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key - assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end @@ -141,5 +145,17 @@ module ActiveRecord assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end + + test "cache_key with a relation having distinct and order" do + developers = Developer.distinct.order(:salary).limit(5) + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + end + + test "cache_key with a relation having custom select and order" do + developers = Developer.select("name AS dev_name").order("dev_name DESC").limit(5) + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + end end end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb index 82c6cf8dea..72838ff56b 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -45,11 +45,11 @@ module ActiveRecord # Make sure the pool marks the connection in use assert_equal @adapter, pool.connection - assert @adapter.in_use? + assert_predicate @adapter, :in_use? # Close should put the adapter back in the pool @adapter.close - assert_not @adapter.in_use? + assert_not_predicate @adapter, :in_use? assert_equal @adapter, pool.connection end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 74d0ed348e..f4cc251fb9 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true require "cases/helper" +require "models/person" module ActiveRecord module ConnectionAdapters class ConnectionHandlerTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :people + def setup @handler = ConnectionHandler.new @spec_name = "primary" @@ -98,11 +103,11 @@ module ActiveRecord end def test_active_connections? - assert !@handler.active_connections? + assert_not_predicate @handler, :active_connections? assert @handler.retrieve_connection(@spec_name) - assert @handler.active_connections? + assert_predicate @handler, :active_connections? @handler.clear_active_connections! - assert !@handler.active_connections? + assert_not_predicate @handler, :active_connections? end def test_retrieve_connection_pool @@ -139,6 +144,75 @@ module ActiveRecord rd.close end + def test_forked_child_doesnt_mangle_parent_connection + object_id = ActiveRecord::Base.connection.object_id + assert_predicate ActiveRecord::Base.connection, :active? + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork { + rd.close + if ActiveRecord::Base.connection.active? + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + end + wr.close + + exit # allow finalizers to run + } + + wr.close + + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + + assert_equal 3, ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people") + end + + unless in_memory_db? + def test_forked_child_recovers_from_disconnected_parent + object_id = ActiveRecord::Base.connection.object_id + assert_predicate ActiveRecord::Base.connection, :active? + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + outer_pid = fork { + ActiveRecord::Base.connection.disconnect! + + pid = fork { + rd.close + if ActiveRecord::Base.connection.active? + pair = [ActiveRecord::Base.connection.object_id, + ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")] + wr.write Marshal.dump pair + end + wr.close + + exit # allow finalizers to run + } + + Process.waitpid pid + } + + wr.close + + Process.waitpid outer_pid + child_id, child_count = Marshal.load(rd.read) + + assert_not_equal object_id, child_id + rd.close + + assert_equal 3, child_count + + # Outer connection is unaffected + assert_equal 6, ActiveRecord::Base.connection.select_value("SELECT 2 * COUNT(*) FROM people") + end + end + def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool @pool.schema_cache = @pool.connection.schema_cache @pool.schema_cache.add("posts") @@ -200,15 +274,15 @@ module ActiveRecord def test_a_class_using_custom_pool_and_switching_back_to_primary klass2 = Class.new(Base) { def self.name; "klass2"; end } - assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + assert_same klass2.connection, ActiveRecord::Base.connection pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) - assert_equal klass2.connection.object_id, pool.connection.object_id - refute_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + assert_same klass2.connection, pool.connection + refute_same klass2.connection, ActiveRecord::Base.connection klass2.remove_connection - assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + assert_same klass2.connection, ActiveRecord::Base.connection end def test_connection_specification_name_should_fallback_to_parent @@ -223,8 +297,8 @@ module ActiveRecord def test_remove_connection_should_not_remove_parent klass2 = Class.new(Base) { def self.name; "klass2"; end } klass2.remove_connection - refute_nil ActiveRecord::Base.connection.object_id - assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + refute_nil ActiveRecord::Base.connection + assert_same klass2.connection, ActiveRecord::Base.connection end end end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 006be9e65d..67496381d1 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -22,8 +22,8 @@ module ActiveRecord new_cache = YAML.load(YAML.dump(@cache)) assert_no_queries do - assert_equal 11, new_cache.columns("posts").size - assert_equal 11, new_cache.columns_hash("posts").size + assert_equal 12, new_cache.columns("posts").size + assert_equal 12, new_cache.columns_hash("posts").size assert new_cache.data_sources("posts") assert_equal "id", new_cache.primary_keys("posts") end @@ -75,8 +75,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) assert_no_queries do - assert_equal 11, @cache.columns("posts").size - assert_equal 11, @cache.columns_hash("posts").size + assert_equal 12, @cache.columns("posts").size + assert_equal 12, @cache.columns_hash("posts").size assert @cache.data_sources("posts") assert_equal "id", @cache.primary_keys("posts") end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 917a04ebc3..1c79d776f0 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -82,11 +82,11 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_bigint_limit - cast_type = @connection.send(:type_map).lookup("bigint") + limit = @connection.send(:type_map).lookup("bigint").send(:_limit) if current_adapter?(:OracleAdapter) - assert_equal 19, cast_type.limit + assert_equal 19, limit else - assert_equal 8, cast_type.limit + assert_equal 8, limit end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 9d6ecbde78..0941ee3309 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -27,7 +27,7 @@ module ActiveRecord # make sure we have an active connection assert ActiveRecord::Base.connection - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end def test_app_delegation @@ -47,14 +47,14 @@ module ActiveRecord def test_connections_are_cleared_after_body_close _, _, body = @management.call(@env) body.close - assert !ActiveRecord::Base.connection_handler.active_connections? + assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections? end def test_active_connections_are_not_cleared_on_body_close_during_transaction ActiveRecord::Base.transaction do _, _, body = @management.call(@env) body.close - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end end @@ -62,7 +62,7 @@ module ActiveRecord app = Class.new(App) { def call(env); raise NotImplementedError; end }.new explosive = middleware(app) assert_raises(NotImplementedError) { explosive.call(@env) } - assert !ActiveRecord::Base.connection_handler.active_connections? + assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections? end def test_connections_not_closed_if_exception_inside_transaction @@ -70,14 +70,14 @@ module ActiveRecord app = Class.new(App) { def call(env); raise RuntimeError; end }.new explosive = middleware(app) assert_raises(RuntimeError) { explosive.call(@env) } - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end end test "doesn't clear active connections when running in a test case" do executor.wrap do @management.call(@env) - assert ActiveRecord::Base.connection_handler.active_connections? + assert_predicate ActiveRecord::Base.connection_handler, :active_connections? end end @@ -85,7 +85,7 @@ module ActiveRecord body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } response_body = middleware(app).call(@env)[2] - assert response_body.respond_to?(:to_path) + assert_respond_to response_body, :to_path assert_equal "/path", response_body.to_path end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 2bfe490602..bfaaa3c54e 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -35,12 +35,12 @@ module ActiveRecord def test_checkout_after_close connection = pool.connection - assert connection.in_use? + assert_predicate connection, :in_use? connection.close - assert !connection.in_use? + assert_not_predicate connection, :in_use? - assert pool.connection.in_use? + assert_predicate pool.connection, :in_use? end def test_released_connection_moves_between_threads @@ -80,18 +80,18 @@ module ActiveRecord end def test_active_connection_in_use - assert !pool.active_connection? + assert_not_predicate pool, :active_connection? main_thread = pool.connection - assert pool.active_connection? + assert_predicate pool, :active_connection? main_thread.close - assert !pool.active_connection? + assert_not_predicate pool, :active_connection? end def test_full_pool_exception - @pool.size.times { @pool.checkout } + @pool.size.times { assert @pool.checkout } assert_raises(ConnectionTimeoutError) do @pool.checkout end @@ -156,13 +156,60 @@ module ActiveRecord @pool.connections.each { |conn| conn.close if conn.in_use? } end + def test_flush + idle_conn = @pool.checkout + recent_conn = @pool.checkout + active_conn = @pool.checkout + + @pool.checkin idle_conn + @pool.checkin recent_conn + + assert_equal 3, @pool.connections.length + + def idle_conn.seconds_idle + 1000 + end + + @pool.flush(30) + + assert_equal 2, @pool.connections.length + + assert_equal [recent_conn, active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__) + ensure + @pool.checkin active_conn + end + + def test_flush_bang + idle_conn = @pool.checkout + recent_conn = @pool.checkout + active_conn = @pool.checkout + _dead_conn = Thread.new { @pool.checkout }.join + + @pool.checkin idle_conn + @pool.checkin recent_conn + + assert_equal 4, @pool.connections.length + + def idle_conn.seconds_idle + 1000 + end + + @pool.flush! + + assert_equal 1, @pool.connections.length + + assert_equal [active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__) + ensure + @pool.checkin active_conn + end + def test_remove_connection conn = @pool.checkout - assert conn.in_use? + assert_predicate conn, :in_use? length = @pool.connections.length @pool.remove conn - assert conn.in_use? + assert_predicate conn, :in_use? assert_equal(length - 1, @pool.connections.length) ensure conn.close @@ -177,11 +224,11 @@ module ActiveRecord end def test_active_connection? - assert !@pool.active_connection? + assert_not_predicate @pool, :active_connection? assert @pool.connection - assert @pool.active_connection? + assert_predicate @pool, :active_connection? @pool.release_connection - assert !@pool.active_connection? + assert_not_predicate @pool, :active_connection? end def test_checkout_behaviour @@ -205,6 +252,14 @@ module ActiveRecord end.join end + def test_checkout_order_is_lifo + conn1 = @pool.checkout + conn2 = @pool.checkout + @pool.checkin conn1 + @pool.checkin conn2 + assert_equal [conn2, conn1], 2.times.map { @pool.checkout } + end + # The connection pool is "fair" if threads waiting for # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request @@ -414,6 +469,7 @@ module ActiveRecord end def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns + Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout [:disconnect, :clear_reloadable_connections].each do |group_action_method| @pool.with_connection do |connection| @@ -422,6 +478,8 @@ module ActiveRecord end end end + ensure + Thread.report_on_exception = original_report_on_exception if Thread.respond_to?(:report_on_exception) end def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns @@ -438,7 +496,7 @@ module ActiveRecord assert_nil timed_join_result # assert that since this is within default timeout our connection hasn't been forcefully taken away from us - assert @pool.active_connection? + assert_predicate @pool, :active_connection? end ensure thread.join if thread && !timed_join_result # clean up the other thread @@ -452,7 +510,7 @@ module ActiveRecord @pool.with_connection do |connection| Thread.new { @pool.send(group_action_method) }.join # assert connection has been forcefully taken away from us - assert_not @pool.active_connection? + assert_not_predicate @pool, :active_connection? # make a new connection for with_connection to clean up @pool.connection diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 4690682cd8..3d11b573f1 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -53,7 +53,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase def test_default_decimal_number record = DefaultNumber.new - assert_equal BigDecimal.new("2.78"), record.decimal_number + assert_equal BigDecimal("2.78"), record.decimal_number assert_equal "2.78", record.decimal_number_before_type_cast end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index a602f83d8c..14c4e3cbd4 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -24,18 +24,18 @@ class DirtyTest < ActiveRecord::TestCase # Change catchphrase. pirate.catchphrase = "arrr" - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? assert_nil pirate.catchphrase_was assert_equal [nil, "arrr"], pirate.catchphrase_change # Saved - no changes. pirate.save! - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? assert_nil pirate.catchphrase_change # Same value - no changes. pirate.catchphrase = "arrr" - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? assert_nil pirate.catchphrase_change end @@ -46,23 +46,23 @@ class DirtyTest < ActiveRecord::TestCase # New record - no changes. pirate = target.new - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Saved - no changes. pirate.catchphrase = "arrrr, time zone!!" pirate.save! - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Change created_on. old_created_on = pirate.created_on pirate.created_on = Time.now - 1.day - assert pirate.created_on_changed? + assert_predicate pirate, :created_on_changed? assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was assert_equal old_created_on, pirate.created_on_was pirate.created_on = old_created_on - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? end end @@ -73,7 +73,7 @@ class DirtyTest < ActiveRecord::TestCase pirate = target.create! pirate.created_on = pirate.created_on - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? end end @@ -86,19 +86,19 @@ class DirtyTest < ActiveRecord::TestCase # New record - no changes. pirate = target.new - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Saved - no changes. pirate.catchphrase = "arrrr, time zone!!" pirate.save! - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Change created_on. old_created_on = pirate.created_on pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? + assert_predicate pirate, :created_on_changed? # kind_of does not work because # ActiveSupport::TimeWithZone.name == 'Time' assert_instance_of Time, pirate.created_on_was @@ -113,19 +113,19 @@ class DirtyTest < ActiveRecord::TestCase # New record - no changes. pirate = target.new - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Saved - no changes. pirate.catchphrase = "arrrr, time zone!!" pirate.save! - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? assert_nil pirate.created_on_change # Change created_on. old_created_on = pirate.created_on pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? + assert_predicate pirate, :created_on_changed? # kind_of does not work because # ActiveSupport::TimeWithZone.name == 'Time' assert_instance_of Time, pirate.created_on_was @@ -137,11 +137,11 @@ class DirtyTest < ActiveRecord::TestCase # the actual attribute here is name, title is an # alias setup via alias_attribute parrot = Parrot.new - assert !parrot.title_changed? + assert_not_predicate parrot, :title_changed? assert_nil parrot.title_change parrot.name = "Sam" - assert parrot.title_changed? + assert_predicate parrot, :title_changed? assert_nil parrot.title_was assert_equal parrot.name_change, parrot.title_change end @@ -153,7 +153,7 @@ class DirtyTest < ActiveRecord::TestCase pirate.restore_catchphrase! assert_equal "Yar!", pirate.catchphrase assert_equal Hash.new, pirate.changes - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? end def test_nullable_number_not_marked_as_changed_if_new_value_is_blank @@ -161,7 +161,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| pirate.parrot_id = value - assert !pirate.parrot_id_changed? + assert_not_predicate pirate, :parrot_id_changed? assert_nil pirate.parrot_id_change end end @@ -171,7 +171,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| numeric_data.bank_balance = value - assert !numeric_data.bank_balance_changed? + assert_not_predicate numeric_data, :bank_balance_changed? assert_nil numeric_data.bank_balance_change end end @@ -181,7 +181,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| numeric_data.temperature = value - assert !numeric_data.temperature_changed? + assert_not_predicate numeric_data, :temperature_changed? assert_nil numeric_data.temperature_change end end @@ -197,7 +197,7 @@ class DirtyTest < ActiveRecord::TestCase ["", nil].each do |value| topic.written_on = value assert_nil topic.written_on - assert !topic.written_on_changed? + assert_not_predicate topic, :written_on_changed? end end end @@ -208,10 +208,10 @@ class DirtyTest < ActiveRecord::TestCase pirate.catchphrase = "arrr" assert pirate.save! - assert !pirate.changed? + assert_not_predicate pirate, :changed? pirate.parrot_id = "0" - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_integer_zero_to_integer_zero_not_marked_as_changed @@ -220,17 +220,17 @@ class DirtyTest < ActiveRecord::TestCase pirate.catchphrase = "arrr" assert pirate.save! - assert !pirate.changed? + assert_not_predicate pirate, :changed? pirate.parrot_id = 0 - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_float_zero_to_string_zero_not_marked_as_changed data = NumericData.new temperature: 0.0 data.save! - assert_not data.changed? + assert_not_predicate data, :changed? data.temperature = "0" assert_empty data.changes @@ -251,38 +251,38 @@ class DirtyTest < ActiveRecord::TestCase # check the change from 1 to '' pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = "" - assert pirate.parrot_id_changed? + assert_predicate pirate, :parrot_id_changed? assert_equal([1, nil], pirate.parrot_id_change) pirate.save # check the change from nil to 0 pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = 0 - assert pirate.parrot_id_changed? + assert_predicate pirate, :parrot_id_changed? assert_equal([nil, 0], pirate.parrot_id_change) pirate.save # check the change from 0 to '' pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = "" - assert pirate.parrot_id_changed? + assert_predicate pirate, :parrot_id_changed? assert_equal([0, nil], pirate.parrot_id_change) end def test_object_should_be_changed_if_any_attribute_is_changed pirate = Pirate.new - assert !pirate.changed? + assert_not_predicate pirate, :changed? assert_equal [], pirate.changed assert_equal Hash.new, pirate.changes pirate.catchphrase = "arrr" - assert pirate.changed? + assert_predicate pirate, :changed? assert_nil pirate.catchphrase_was assert_equal %w(catchphrase), pirate.changed assert_equal({ "catchphrase" => [nil, "arrr"] }, pirate.changes) pirate.save - assert !pirate.changed? + assert_not_predicate pirate, :changed? assert_equal [], pirate.changed assert_equal Hash.new, pirate.changes end @@ -290,40 +290,40 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(catchphrase: "arr") - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :catchphrase_changed? assert pirate.catchphrase_will_change! - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? assert_equal ["arr", "arr"], pirate.catchphrase_change pirate.catchphrase << " matey!" - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? assert_equal ["arr", "arr matey!"], pirate.catchphrase_change end def test_virtual_attribute_will_change parrot = Parrot.create!(name: "Ruby") parrot.send(:attribute_will_change!, :cancel_save_from_callback) - assert parrot.has_changes_to_save? + assert_predicate parrot, :has_changes_to_save? end def test_association_assignment_changes_foreign_key pirate = Pirate.create!(catchphrase: "jarl") pirate.parrot = Parrot.create!(name: "Lorre") - assert pirate.changed? + assert_predicate pirate, :changed? assert_equal %w(parrot_id), pirate.changed end def test_attribute_should_be_compared_with_type_cast topic = Topic.new - assert topic.approved? - assert !topic.approved_changed? + assert_predicate topic, :approved? + assert_not_predicate topic, :approved_changed? # Coming from web form. params = { topic: { approved: 1 } } # In the controller. topic.attributes = params[:topic] - assert topic.approved? - assert !topic.approved_changed? + assert_predicate topic, :approved? + assert_not_predicate topic, :approved_changed? end def test_partial_update @@ -378,9 +378,9 @@ class DirtyTest < ActiveRecord::TestCase def test_reload_should_clear_changed_attributes pirate = Pirate.create!(catchphrase: "shiver me timbers") pirate.catchphrase = "*hic*" - assert pirate.changed? + assert_predicate pirate, :changed? pirate.reload - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_dup_objects_should_not_copy_dirty_flag_from_creator @@ -388,17 +388,17 @@ class DirtyTest < ActiveRecord::TestCase pirate_dup = pirate.dup pirate_dup.restore_catchphrase! pirate.catchphrase = "I love Rum" - assert pirate.catchphrase_changed? - assert !pirate_dup.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? + assert_not_predicate pirate_dup, :catchphrase_changed? end def test_reverted_changes_are_not_dirty phrase = "shiver me timbers" pirate = Pirate.create!(catchphrase: phrase) pirate.catchphrase = "*hic*" - assert pirate.changed? + assert_predicate pirate, :changed? pirate.catchphrase = phrase - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_reverted_changes_are_not_dirty_after_multiple_changes @@ -406,40 +406,40 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(catchphrase: phrase) 10.times do |i| pirate.catchphrase = "*hic*" * i - assert pirate.changed? + assert_predicate pirate, :changed? end - assert pirate.changed? + assert_predicate pirate, :changed? pirate.catchphrase = phrase - assert !pirate.changed? + assert_not_predicate pirate, :changed? end def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back pirate = Pirate.create!(catchphrase: "Yar!") pirate.parrot_id = 1 - assert pirate.changed? - assert pirate.parrot_id_changed? - assert !pirate.catchphrase_changed? + assert_predicate pirate, :changed? + assert_predicate pirate, :parrot_id_changed? + assert_not_predicate pirate, :catchphrase_changed? pirate.parrot_id = nil - assert !pirate.changed? - assert !pirate.parrot_id_changed? - assert !pirate.catchphrase_changed? + assert_not_predicate pirate, :changed? + assert_not_predicate pirate, :parrot_id_changed? + assert_not_predicate pirate, :catchphrase_changed? end def test_save_should_store_serialized_attributes_even_with_partial_writes with_partial_writes(Topic) do topic = Topic.create!(content: { a: "a" }) - assert_not topic.changed? + assert_not_predicate topic, :changed? topic.content[:b] = "b" - assert topic.changed? + assert_predicate topic, :changed? topic.save! - assert_not topic.changed? + assert_not_predicate topic, :changed? assert_equal "b", topic.content[:b] topic.reload @@ -596,7 +596,7 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(catchphrase: "rrrr", created_on: time_in_paris) pirate.created_on = pirate.created_on.in_time_zone("Tokyo").to_s - assert !pirate.created_on_changed? + assert_not_predicate pirate, :created_on_changed? end test "partial insert" do @@ -627,7 +627,7 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? expected_changes = { "catchphrase" => ["arrrr", "arrrr matey!"] } @@ -641,7 +641,7 @@ class DirtyTest < ActiveRecord::TestCase pirate.reload assert_equal "arrrr matey!", pirate.catchphrase - assert_not pirate.changed? + assert_not_predicate pirate, :changed? end test "in place mutation for binary" do @@ -652,19 +652,19 @@ class DirtyTest < ActiveRecord::TestCase binary = klass.create!(data: "\\\\foo") - assert_not binary.changed? + assert_not_predicate binary, :changed? binary.data = binary.data.dup - assert_not binary.changed? + assert_not_predicate binary, :changed? binary = klass.last - assert_not binary.changed? + assert_not_predicate binary, :changed? binary.data << "bar" - assert binary.changed? + assert_predicate binary, :changed? end test "changes is correct for subclass" do @@ -679,7 +679,7 @@ class DirtyTest < ActiveRecord::TestCase new_catchphrase = "arrrr matey!" pirate.catchphrase = new_catchphrase - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? expected_changes = { "catchphrase" => ["arrrr", new_catchphrase] @@ -698,7 +698,7 @@ class DirtyTest < ActiveRecord::TestCase new_catchphrase = "arrrr matey!" pirate.catchphrase = new_catchphrase - assert pirate.catchphrase_changed? + assert_predicate pirate, :catchphrase_changed? expected_changes = { "catchphrase" => ["arrrr", new_catchphrase] @@ -720,7 +720,7 @@ class DirtyTest < ActiveRecord::TestCase end model = klass.new(first_name: "Jim") - assert model.first_name_changed? + assert_predicate model, :first_name_changed? end test "attribute_will_change! doesn't try to save non-persistable attributes" do @@ -732,7 +732,7 @@ class DirtyTest < ActiveRecord::TestCase record = klass.new(first_name: "Sean") record.non_persisted_attribute_will_change! - assert record.non_persisted_attribute_changed? + assert_predicate record, :non_persisted_attribute_changed? assert record.save end @@ -762,26 +762,33 @@ class DirtyTest < ActiveRecord::TestCase test "attributes assigned but not selected are dirty" do person = Person.select(:id).first - refute person.changed? + assert_not_predicate person, :changed? person.first_name = "Sean" - assert person.changed? + assert_predicate person, :changed? person.first_name = nil - assert person.changed? + assert_predicate person, :changed? + end + + test "attributes not selected are still missing after save" do + person = Person.select(:id).first + assert_raises(ActiveModel::MissingAttributeError) { person.first_name } + assert person.save # calls forget_attribute_assignments + assert_raises(ActiveModel::MissingAttributeError) { person.first_name } end test "saved_change_to_attribute? returns whether a change occurred in the last save" do person = Person.create!(first_name: "Sean") - assert person.saved_change_to_first_name? - refute person.saved_change_to_gender? + assert_predicate person, :saved_change_to_first_name? + assert_not_predicate person, :saved_change_to_gender? assert person.saved_change_to_first_name?(from: nil, to: "Sean") assert person.saved_change_to_first_name?(from: nil) assert person.saved_change_to_first_name?(to: "Sean") - refute person.saved_change_to_first_name?(from: "Jim", to: "Sean") - refute person.saved_change_to_first_name?(from: "Jim") - refute person.saved_change_to_first_name?(to: "Jim") + assert_not person.saved_change_to_first_name?(from: "Jim", to: "Sean") + assert_not person.saved_change_to_first_name?(from: "Jim") + assert_not person.saved_change_to_first_name?(to: "Jim") end test "saved_change_to_attribute returns the change that occurred in the last save" do @@ -816,11 +823,11 @@ class DirtyTest < ActiveRecord::TestCase test "saved_changes? returns whether the last call to save changed anything" do person = Person.create!(first_name: "Sean") - assert person.saved_changes? + assert_predicate person, :saved_changes? person.save - refute person.saved_changes? + assert_not_predicate person, :saved_changes? end test "saved_changes returns a hash of all the changes that occurred" do @@ -850,7 +857,7 @@ class DirtyTest < ActiveRecord::TestCase end person = klass.create!(first_name: "Sean") - refute person.changed? + assert_not_predicate person, :changed? end private @@ -863,8 +870,8 @@ class DirtyTest < ActiveRecord::TestCase end def check_pirate_after_save_failure(pirate) - assert pirate.changed? - assert pirate.parrot_id_changed? + assert_predicate pirate, :changed? + assert_predicate pirate, :parrot_id_changed? assert_equal %w(parrot_id), pirate.changed assert_nil pirate.parrot_id_was end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 2fefdbf204..9e33c3110c 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -3,13 +3,14 @@ require "cases/helper" require "models/reply" require "models/topic" +require "models/movie" module ActiveRecord class DupTest < ActiveRecord::TestCase fixtures :topics def test_dup - assert !Topic.new.freeze.dup.frozen? + assert_not_predicate Topic.new.freeze.dup, :frozen? end def test_not_readonly @@ -40,7 +41,7 @@ module ActiveRecord topic.destroy duped = topic.dup - assert_not duped.destroyed? + assert_not_predicate duped, :destroyed? end def test_dup_has_no_id @@ -62,10 +63,10 @@ module ActiveRecord topic.attributes = dbtopic.attributes.except("id") - #duped has no timestamp values + # duped has no timestamp values duped = dbtopic.dup - #clear topic timestamp values + # clear topic timestamp values topic.send(:clear_timestamp_attributes) assert_equal topic.changes, duped.changes @@ -100,7 +101,7 @@ module ActiveRecord # temporary change to the topic object topic.updated_at -= 3.days - #dup should not preserve the timestamps if present + # dup should not preserve the timestamps if present new_topic = topic.dup assert_nil new_topic.updated_at assert_nil new_topic.created_at @@ -126,12 +127,12 @@ module ActiveRecord duped = topic.dup duped.title = nil - assert duped.invalid? + assert_predicate duped, :invalid? topic.title = nil duped.title = "Mathematics" - assert topic.invalid? - assert duped.valid? + assert_predicate topic, :invalid? + assert_predicate duped, :valid? end end @@ -157,5 +158,20 @@ module ActiveRecord record.dup end end + + def test_dup_record_not_persisted_after_rollback_transaction + movie = Movie.new(name: "test") + + assert_raises(ActiveRecord::RecordInvalid) do + Movie.transaction do + movie.save! + duped = movie.dup + duped.name = nil + duped.save! + end + end + + assert !movie.persisted? + end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 78cb89ccc5..d5a1d11e12 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -12,16 +12,16 @@ class EnumTest < ActiveRecord::TestCase end test "query state by predicate" do - assert @book.published? - assert_not @book.written? - assert_not @book.proposed? + assert_predicate @book, :published? + assert_not_predicate @book, :written? + assert_not_predicate @book, :proposed? - assert @book.read? - assert @book.in_english? - assert @book.author_visibility_visible? - assert @book.illustrator_visibility_visible? - assert @book.with_medium_font_size? - assert @book.medium_to_read? + assert_predicate @book, :read? + assert_predicate @book, :in_english? + assert_predicate @book, :author_visibility_visible? + assert_predicate @book, :illustrator_visibility_visible? + assert_predicate @book, :with_medium_font_size? + assert_predicate @book, :medium_to_read? end test "query state with strings" do @@ -76,46 +76,46 @@ class EnumTest < ActiveRecord::TestCase end test "build from scope" do - assert Book.written.build.written? - assert_not Book.written.build.proposed? + assert_predicate Book.written.build, :written? + assert_not_predicate Book.written.build, :proposed? end test "build from where" do - assert Book.where(status: Book.statuses[:written]).build.written? - assert_not Book.where(status: Book.statuses[:written]).build.proposed? - assert Book.where(status: :written).build.written? - assert_not Book.where(status: :written).build.proposed? - assert Book.where(status: "written").build.written? - assert_not Book.where(status: "written").build.proposed? + assert_predicate Book.where(status: Book.statuses[:written]).build, :written? + assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed? + assert_predicate Book.where(status: :written).build, :written? + assert_not_predicate Book.where(status: :written).build, :proposed? + assert_predicate Book.where(status: "written").build, :written? + assert_not_predicate Book.where(status: "written").build, :proposed? end test "update by declaration" do @book.written! - assert @book.written? + assert_predicate @book, :written? @book.in_english! - assert @book.in_english? + assert_predicate @book, :in_english? @book.author_visibility_visible! - assert @book.author_visibility_visible? + assert_predicate @book, :author_visibility_visible? end test "update by setter" do @book.update! status: :written - assert @book.written? + assert_predicate @book, :written? end test "enum methods are overwritable" do assert_equal "do publish work...", @book.published! - assert @book.published? + assert_predicate @book, :published? end test "direct assignment" do @book.status = :written - assert @book.written? + assert_predicate @book, :written? end test "assign string value" do @book.status = "written" - assert @book.written? + assert_predicate @book, :written? end test "enum changed attributes" do @@ -242,17 +242,17 @@ class EnumTest < ActiveRecord::TestCase end test "building new objects with enum scopes" do - assert Book.written.build.written? - assert Book.read.build.read? - assert Book.in_spanish.build.in_spanish? - assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible? + assert_predicate Book.written.build, :written? + assert_predicate Book.read.build, :read? + assert_predicate Book.in_spanish.build, :in_spanish? + assert_predicate Book.illustrator_visibility_invisible.build, :illustrator_visibility_invisible? end test "creating new objects with enum scopes" do - assert Book.written.create.written? - assert Book.read.create.read? - assert Book.in_spanish.create.in_spanish? - assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible? + assert_predicate Book.written.create, :written? + assert_predicate Book.read.create, :read? + assert_predicate Book.in_spanish.create, :in_spanish? + assert_predicate Book.illustrator_visibility_invisible.create, :illustrator_visibility_invisible? end test "_before_type_cast" do @@ -308,6 +308,24 @@ class EnumTest < ActiveRecord::TestCase end end + test "reserved enum values for relation" do + relation_method_samples = [ + :records, + :to_ary, + :scope_for_create + ] + + relation_method_samples.each do |value| + e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum category: [:other, value] + end + end + assert_match(/You tried to define an enum named .* on the model/, e.message) + end + end + test "overriding enum method should not raise" do assert_nothing_raised do Class.new(ActiveRecord::Base) do @@ -337,9 +355,9 @@ class EnumTest < ActiveRecord::TestCase klass.delete_all klass.create!(status: "proposed") book = klass.new(status: "written") - assert book.valid? + assert_predicate book, :valid? book.status = "proposed" - assert_not book.valid? + assert_not_predicate book, :valid? end test "validate inclusion of value in array" do @@ -350,9 +368,9 @@ class EnumTest < ActiveRecord::TestCase end klass.delete_all invalid_book = klass.new(status: "proposed") - assert_not invalid_book.valid? + assert_not_predicate invalid_book, :valid? valid_book = klass.new(status: "written") - assert valid_book.valid? + assert_predicate valid_book, :valid? end test "enums are distinct per class" do @@ -399,10 +417,10 @@ class EnumTest < ActiveRecord::TestCase end book1 = klass.proposed.create! - assert book1.proposed? + assert_predicate book1, :proposed? book2 = klass.single.create! - assert book2.single? + assert_predicate book2, :single? end test "enum with alias_attribute" do @@ -413,62 +431,62 @@ class EnumTest < ActiveRecord::TestCase end book = klass.proposed.create! - assert book.proposed? + assert_predicate book, :proposed? assert_equal "proposed", book.aliased_status book = klass.find(book.id) - assert book.proposed? + assert_predicate book, :proposed? assert_equal "proposed", book.aliased_status end test "query state by predicate with prefix" do - assert @book.author_visibility_visible? - assert_not @book.author_visibility_invisible? - assert @book.illustrator_visibility_visible? - assert_not @book.illustrator_visibility_invisible? + assert_predicate @book, :author_visibility_visible? + assert_not_predicate @book, :author_visibility_invisible? + assert_predicate @book, :illustrator_visibility_visible? + assert_not_predicate @book, :illustrator_visibility_invisible? end test "query state by predicate with custom prefix" do - assert @book.in_english? - assert_not @book.in_spanish? - assert_not @book.in_french? + assert_predicate @book, :in_english? + assert_not_predicate @book, :in_spanish? + assert_not_predicate @book, :in_french? end test "query state by predicate with custom suffix" do - assert @book.medium_to_read? - assert_not @book.easy_to_read? - assert_not @book.hard_to_read? + assert_predicate @book, :medium_to_read? + assert_not_predicate @book, :easy_to_read? + assert_not_predicate @book, :hard_to_read? end test "enum methods with custom suffix defined" do - assert @book.class.respond_to?(:easy_to_read) - assert @book.class.respond_to?(:medium_to_read) - assert @book.class.respond_to?(:hard_to_read) + assert_respond_to @book.class, :easy_to_read + assert_respond_to @book.class, :medium_to_read + assert_respond_to @book.class, :hard_to_read - assert @book.respond_to?(:easy_to_read?) - assert @book.respond_to?(:medium_to_read?) - assert @book.respond_to?(:hard_to_read?) + assert_respond_to @book, :easy_to_read? + assert_respond_to @book, :medium_to_read? + assert_respond_to @book, :hard_to_read? - assert @book.respond_to?(:easy_to_read!) - assert @book.respond_to?(:medium_to_read!) - assert @book.respond_to?(:hard_to_read!) + assert_respond_to @book, :easy_to_read! + assert_respond_to @book, :medium_to_read! + assert_respond_to @book, :hard_to_read! end test "update enum attributes with custom suffix" do @book.medium_to_read! - assert_not @book.easy_to_read? - assert @book.medium_to_read? - assert_not @book.hard_to_read? + assert_not_predicate @book, :easy_to_read? + assert_predicate @book, :medium_to_read? + assert_not_predicate @book, :hard_to_read? @book.easy_to_read! - assert @book.easy_to_read? - assert_not @book.medium_to_read? - assert_not @book.hard_to_read? + assert_predicate @book, :easy_to_read? + assert_not_predicate @book, :medium_to_read? + assert_not_predicate @book, :hard_to_read? @book.hard_to_read! - assert_not @book.easy_to_read? - assert_not @book.medium_to_read? - assert @book.hard_to_read? + assert_not_predicate @book, :easy_to_read? + assert_not_predicate @book, :medium_to_read? + assert_predicate @book, :hard_to_read? end test "uses default status when no status is provided in fixtures" do @@ -479,12 +497,12 @@ class EnumTest < ActiveRecord::TestCase test "uses default value from database on initialization" do book = Book.new - assert book.proposed? + assert_predicate book, :proposed? end test "uses default value from database on initialization when using custom mapping" do book = Book.new - assert book.hard? + assert_predicate book, :hard? end test "data type of Enum type" do diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index fb698c47cd..82cc891970 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -15,20 +15,20 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_the_payload_has_an_exception SUBSCRIBER.finish(nil, nil, exception: Exception.new) - assert queries.empty? + assert_empty queries end def test_collects_nothing_for_ignored_payloads ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| SUBSCRIBER.finish(nil, nil, name: ip) end - assert queries.empty? + assert_empty queries end def test_collects_nothing_if_collect_is_false ActiveRecord::ExplainRegistry.collect = false SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select 1 from users", binds: [1, 2]) - assert queries.empty? + assert_empty queries end def test_collects_pairs_of_queries_and_binds @@ -42,12 +42,12 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_the_statement_is_not_whitelisted SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length") - assert queries.empty? + assert_empty queries end def test_collects_nothing_if_the_statement_is_only_partially_matched SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select_db yo_mama") - assert queries.empty? + assert_empty queries end def test_collects_cte_queries diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 4039af66d0..59af4e6961 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -8,7 +8,7 @@ class FinderRespondToTest < ActiveRecord::TestCase def test_should_preserve_normal_respond_to_behaviour_on_base assert_respond_to ActiveRecord::Base, :new - assert !ActiveRecord::Base.respond_to?(:find_by_something) + assert_not_respond_to ActiveRecord::Base, :find_by_something end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method @@ -43,14 +43,14 @@ class FinderRespondToTest < ActiveRecord::TestCase end def test_should_not_respond_to_find_by_one_missing_attribute - assert !Topic.respond_to?(:find_by_undertitle) + assert_not_respond_to Topic, :find_by_undertitle end def test_should_not_respond_to_find_by_invalid_method_syntax - assert !Topic.respond_to?(:fail_to_find_by_title) - assert !Topic.respond_to?(:find_by_title?) - assert !Topic.respond_to?(:fail_to_find_or_create_by_title) - assert !Topic.respond_to?(:find_or_create_by_title?) + assert_not_respond_to Topic, :fail_to_find_by_title + assert_not_respond_to Topic, :find_by_title? + assert_not_respond_to Topic, :fail_to_find_or_create_by_title + assert_not_respond_to Topic, :find_or_create_by_title? end private diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index d8bc917e7f..b413212e26 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -120,6 +120,21 @@ class FinderTest < ActiveRecord::TestCase assert_equal "The Fourth Topic of the day", records[2].title end + def test_find_with_ids_with_no_id_passed + exception = assert_raises(ActiveRecord::RecordNotFound) { Topic.find } + assert_equal exception.model, "Topic" + assert_equal exception.primary_key, "id" + end + + def test_find_with_ids_with_id_out_of_range + exception = assert_raises(ActiveRecord::RecordNotFound) do + Topic.find("9999999999999999999999999999999") + end + + assert_equal exception.model, "Topic" + assert_equal exception.primary_key, "id" + end + def test_find_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.find(Topic.last) @@ -239,7 +254,7 @@ class FinderTest < ActiveRecord::TestCase # Ensure +exists?+ runs without an error by excluding order value. def test_exists_with_order - assert_equal true, Topic.order("invalid sql here").exists? + assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists? end def test_exists_with_joins @@ -255,27 +270,27 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_with_includes_limit_and_empty_result - assert_equal false, Topic.includes(:replies).limit(0).exists? - assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? + assert_no_queries { assert_equal false, Topic.includes(:replies).limit(0).exists? } + assert_queries(1) { assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? } end def test_exists_with_distinct_association_includes_and_limit author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists? + unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments) + assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? } + assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? } end def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(1).exists? + unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC") + assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? } + assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? } end def test_exists_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association - assert_nothing_raised do - developer = developers(:david) - developer.ratings.includes(comment: :post).where(posts: { id: 1 }).exists? - end + ratings = developers(:david).ratings.includes(comment: :post).where(posts: { id: 1 }) + assert_queries(1) { assert_not_predicate ratings.limit(1), :exists? } end def test_exists_with_empty_table_and_no_args_given @@ -550,7 +565,7 @@ class FinderTest < ActiveRecord::TestCase assert_nil Topic.offset(4).second_to_last assert_nil Topic.offset(5).second_to_last - #test with limit + # test with limit assert_nil Topic.limit(1).second assert_nil Topic.limit(1).second_to_last end @@ -633,13 +648,13 @@ class FinderTest < ActiveRecord::TestCase def test_last_with_integer_and_order_should_use_sql_limit relation = Topic.order("title") assert_queries(1) { relation.last(5) } - assert !relation.loaded? + assert_not_predicate relation, :loaded? end def test_last_with_integer_and_reorder_should_use_sql_limit relation = Topic.reorder("title") assert_queries(1) { relation.last(5) } - assert !relation.loaded? + assert_not_predicate relation, :loaded? end def test_last_on_loaded_relation_should_not_use_sql @@ -652,7 +667,7 @@ class FinderTest < ActiveRecord::TestCase def test_last_with_irreversible_order assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("coalesce(author_name, title)").last + Topic.order(Arel.sql("coalesce(author_name, title)")).last end end @@ -664,12 +679,34 @@ class FinderTest < ActiveRecord::TestCase assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + assert_equal comments.offset(2).to_a.last, comments.offset(2).last + assert_equal comments.offset(2).to_a.last(2), comments.offset(2).last(2) + assert_equal comments.offset(2).to_a.last(3), comments.offset(2).last(3) + comments = comments.offset(1) assert_equal comments.limit(2).to_a.last, comments.limit(2).last assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end + def test_first_on_relation_with_limit_and_offset + post = posts("sti_comments") + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.first, comments.limit(2).first + assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2) + assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3) + + assert_equal comments.offset(2).to_a.first, comments.offset(2).first + assert_equal comments.offset(2).to_a.first(2), comments.offset(2).first(2) + assert_equal comments.offset(2).to_a.first(3), comments.offset(2).first(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.first, comments.limit(2).first + assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2) + assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3) + end + def test_take_and_first_and_last_with_integer_should_return_an_array assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) @@ -831,6 +868,25 @@ class FinderTest < ActiveRecord::TestCase assert_equal customers(:david), found_customer end + def test_hash_condition_find_with_aggregate_having_three_mappings_array + david_address = customers(:david).address + zaphod_address = customers(:zaphod).address + barney_address = customers(:barney).address + assert_kind_of Address, david_address + assert_kind_of Address, zaphod_address + found_customers = Customer.where(address: [david_address, zaphod_address, barney_address]) + assert_equal [customers(:david), customers(:zaphod), customers(:barney)], found_customers.sort_by(&:id) + end + + def test_hash_condition_find_with_aggregate_having_one_mapping_array + david_balance = customers(:david).balance + zaphod_balance = customers(:zaphod).balance + assert_kind_of Money, david_balance + assert_kind_of Money, zaphod_balance + found_customers = Customer.where(balance: [david_balance, zaphod_balance]) + assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id) + end + def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location @@ -1036,14 +1092,6 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") } end - def test_find_last_with_offset - devs = Developer.order("id") - - assert_equal devs[2], Developer.offset(2).first - assert_equal devs[-3], Developer.offset(2).last - assert_equal devs[-3], Developer.offset(2).order("id DESC").first - end - def test_find_by_nil_attribute topic = Topic.find_by_last_read nil assert_not_nil topic @@ -1223,7 +1271,7 @@ class FinderTest < ActiveRecord::TestCase test "find_by with associations" do assert_equal authors(:david), Post.find_by(author: authors(:david)).author - assert_equal authors(:mary) , Post.find_by(author: authors(:mary)).author + assert_equal authors(:mary), Post.find_by(author: authors(:mary)).author end test "find_by doesn't have implicit ordering" do @@ -1288,12 +1336,12 @@ class FinderTest < ActiveRecord::TestCase test "#skip_query_cache! for #exists? with a limited eager load" do Topic.cache do - assert_queries(2) do + assert_queries(1) do Topic.eager_load(:replies).limit(1).exists? Topic.eager_load(:replies).limit(1).exists? end - assert_queries(4) do + assert_queries(2) do Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index b0b63f5203..c88b90c8eb 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -79,6 +79,151 @@ class FixturesTest < ActiveRecord::TestCase ActiveSupport::Notifications.unsubscribe(subscription) end end + + def test_bulk_insert_multiple_table_with_a_multi_statement_query + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + + create_fixtures("bulbs", "authors", "computers") + + expected_sql = <<-EOS.strip_heredoc.chop + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .* + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .* + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .* + EOS + assert_equal 1, subscriber.events.size + assert_match(/#{expected_sql}/, subscriber.events.first) + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + def test_bulk_insert_with_a_multi_statement_query_raises_an_exception_when_any_insert_fails + require "models/aircraft" + + assert_equal false, Aircraft.columns_hash["wheels_count"].null + fixtures = { + "aircraft" => [ + { "name" => "working_aircrafts", "wheels_count" => 2 }, + { "name" => "broken_aircrafts", "wheels_count" => nil }, + ] + } + + assert_no_difference "Aircraft.count" do + assert_raises(ActiveRecord::NotNullViolation) do + ActiveRecord::Base.connection.insert_fixtures_set(fixtures) + end + end + end + end + + if current_adapter?(:Mysql2Adapter) + def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size + conn = ActiveRecord::Base.connection + mysql_margin = 2 + packet_size = 1024 + bytes_needed_to_have_a_1024_bytes_fixture = 858 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size - mysql_margin) + + error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) } + assert_match(/Fixtures set is too large #{packet_size}\./, error.message) + end + + def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 51] }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size) + + assert_difference "TrafficLight.count" do + conn.insert_fixtures_set(fixtures) + end + end + + def test_insert_fixtures_set_split_the_total_sql_into_two_chunks_smaller_than_max_allowed_packet + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 450] }, + ], + "comments" => [ + { "post_id" => 1, "body" => "a" * 450 }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size) + + conn.insert_fixtures_set(fixtures) + assert_equal 2, subscriber.events.size + assert_operator subscriber.events.first.bytesize, :<, packet_size + assert_operator subscriber.events.second.bytesize, :<, packet_size + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + def test_insert_fixtures_set_concat_total_sql_into_a_single_packet_smaller_than_max_allowed_packet + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 200] }, + ], + "comments" => [ + { "post_id" => 1, "body" => "a" * 200 }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size) + + assert_difference ["TrafficLight.count", "Comment.count"], +1 do + conn.insert_fixtures_set(fixtures) + end + assert_equal 1, subscriber.events.size + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + end + + def test_auto_value_on_primary_key + fixtures = [ + { "name" => "first", "wheels_count" => 2 }, + { "name" => "second", "wheels_count" => 3 } + ] + conn = ActiveRecord::Base.connection + assert_nothing_raised do + conn.insert_fixtures_set({ "aircraft" => fixtures }, ["aircraft"]) + end + result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id") + assert_equal fixtures, result.to_a + end + + def test_deprecated_insert_fixtures + fixtures = [ + { "name" => "first", "wheels_count" => 2 }, + { "name" => "second", "wheels_count" => 3 } + ] + conn = ActiveRecord::Base.connection + conn.delete("DELETE FROM aircraft") + assert_deprecated do + conn.insert_fixtures(fixtures, "aircraft") + end + result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id") + assert_equal fixtures, result.to_a end def test_broken_yaml_exception @@ -247,8 +392,8 @@ class FixturesTest < ActiveRecord::TestCase def test_nonexistent_fixture_file nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere" - #sanity check to make sure that this file never exists - assert Dir[nonexistent_fixture_path + "*"].empty? + # sanity check to make sure that this file never exists + assert_empty Dir[nonexistent_fixture_path + "*"] assert_raise(Errno::ENOENT) do ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) @@ -998,10 +1143,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_resolves_enums - assert books(:awdr).published? - assert books(:awdr).read? - assert books(:rfr).proposed? - assert books(:ddd).published? + assert_predicate books(:awdr), :published? + assert_predicate books(:awdr), :read? + assert_predicate books(:rfr), :proposed? + assert_predicate books(:ddd), :published? end end diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb index 5e503272e1..b15e1b48c4 100644 --- a/activerecord/test/cases/habtm_destroy_order_test.rb +++ b/activerecord/test/cases/habtm_destroy_order_test.rb @@ -15,7 +15,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase sicp.destroy end end - assert !sicp.destroyed? + assert_not_predicate sicp, :destroyed? end test "should not raise error if have foreign key in the join table" do @@ -42,7 +42,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase ben.lessons << sicp ben.save! ben.destroy - assert !ben.reload.lessons.empty? + assert_not_empty ben.reload.lessons ensure # get rid of it so Student is still like it was Student.reset_callbacks(:destroy) @@ -58,6 +58,6 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase assert_raises LessonError do sicp.destroy end - assert !sicp.reload.students.empty? + assert_not_empty sicp.reload.students end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index c931f7d21c..e3ca79af99 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -130,46 +130,46 @@ class InheritanceTest < ActiveRecord::TestCase end def test_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? + assert_not_predicate ActiveRecord::Base, :descends_from_active_record? # Abstract subclass of AR::Base. - assert LoosePerson.descends_from_active_record? + assert_predicate LoosePerson, :descends_from_active_record? # Concrete subclass of an abstract class. - assert LooseDescendant.descends_from_active_record? + assert_predicate LooseDescendant, :descends_from_active_record? # Concrete subclass of AR::Base. - assert TightPerson.descends_from_active_record? + assert_predicate TightPerson, :descends_from_active_record? # Concrete subclass of a concrete class but has no type column. - assert TightDescendant.descends_from_active_record? + assert_predicate TightDescendant, :descends_from_active_record? # Concrete subclass of AR::Base. - assert Post.descends_from_active_record? + assert_predicate Post, :descends_from_active_record? # Concrete subclasses of a concrete class which has a type column. - assert !StiPost.descends_from_active_record? - assert !SubStiPost.descends_from_active_record? + assert_not_predicate StiPost, :descends_from_active_record? + assert_not_predicate SubStiPost, :descends_from_active_record? # Abstract subclass of a concrete class which has a type column. # This is pathological, as you'll never have Sub < Abstract < Concrete. - assert !AbstractStiPost.descends_from_active_record? + assert_not_predicate AbstractStiPost, :descends_from_active_record? # Concrete subclass of an abstract class which has a type column. - assert !SubAbstractStiPost.descends_from_active_record? + assert_not_predicate SubAbstractStiPost, :descends_from_active_record? end def test_company_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? + assert_not_predicate ActiveRecord::Base, :descends_from_active_record? assert AbstractCompany.descends_from_active_record?, "AbstractCompany should descend from ActiveRecord::Base" assert Company.descends_from_active_record?, "Company should descend from ActiveRecord::Base" assert !Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base" end def test_abstract_class - assert !ActiveRecord::Base.abstract_class? - assert LoosePerson.abstract_class? - assert !LooseDescendant.abstract_class? + assert_not_predicate ActiveRecord::Base, :abstract_class? + assert_predicate LoosePerson, :abstract_class? + assert_not_predicate LooseDescendant, :abstract_class? end def test_inheritance_base_class @@ -280,6 +280,21 @@ class InheritanceTest < ActiveRecord::TestCase assert_equal Firm, firm.class end + def test_where_new_with_subclass + firm = Company.where(type: "Firm").new + assert_equal Firm, firm.class + end + + def test_where_create_with_subclass + firm = Company.where(type: "Firm").create(name: "Basecamp") + assert_equal Firm, firm.class + end + + def test_where_create_bang_with_subclass + firm = Company.where(type: "Firm").create!(name: "Basecamp") + assert_equal Firm, firm.class + end + def test_new_with_abstract_class e = assert_raises(NotImplementedError) do AbstractCompany.new @@ -302,6 +317,30 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(type: "Account") } end + def test_where_new_with_invalid_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").new } + end + + def test_where_new_with_unrelated_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").new } + end + + def test_where_create_with_invalid_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create } + end + + def test_where_create_with_unrelated_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create } + end + + def test_where_create_bang_with_invalid_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create! } + end + + def test_where_create_bang_with_unrelated_type + assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create! } + end + def test_new_with_unrelated_namespaced_type without_store_full_sti_class do e = assert_raises ActiveRecord::SubclassNotFound do diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 60c628511f..ebe0b0aa87 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -161,6 +161,15 @@ module ActiveRecord end end + class UpOnlyMigration < SilentMigration + def change + add_column :horses, :oldie, :integer, default: 0 + up_only { execute "update horses set oldie = 1" } + end + end + + self.use_transactional_tests = false + setup do @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false end @@ -378,5 +387,23 @@ module ActiveRecord "horses_index_named index should not exist" end end + + def test_up_only + InvertibleMigration.new.migrate(:up) + horse1 = Horse.create + # populates existing horses with oldie = 1 but new ones have default 0 + UpOnlyMigration.new.migrate(:up) + Horse.reset_column_information + horse1.reload + horse2 = Horse.create + + assert 1, horse1.oldie # created before migration + assert 0, horse2.oldie # created after migration + + UpOnlyMigration.new.migrate(:down) # should be no error + connection = ActiveRecord::Base.connection + assert !connection.column_exists?(:horses, :oldie) + Horse.reset_column_information + end end end diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb index 63f3c77fc3..afc39d0420 100644 --- a/activerecord/test/cases/json_attribute_test.rb +++ b/activerecord/test/cases/json_attribute_test.rb @@ -19,14 +19,14 @@ class JsonAttributeTest < ActiveRecord::TestCase def setup super @connection.create_table("json_data_type") do |t| - t.text "payload" - t.text "settings" + t.string "payload" + t.string "settings" end end private def column_type - :text + :string end def klass diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 52fe488cd5..82cf281cff 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -252,7 +252,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase def @david.favorite_quote; "Constraints are liberating"; end json = @david.to_json(include: :posts, methods: :favorite_quote) - assert !@david.posts.first.respond_to?(:favorite_quote) + assert_not_respond_to @david.posts.first, :favorite_quote assert_match %r{"favorite_quote":"Constraints are liberating"}, json assert_equal 1, %r{"favorite_quote":}.match(json).size end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index a71485982c..c60a276850 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -23,25 +23,23 @@ module JSONSharedTestCases def test_column column = klass.columns_hash["payload"] assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type + assert_type_match column_type, column.sql_type type = klass.type_for_attribute("payload") - assert_not type.binary? + assert_not_predicate type, :binary? end def test_change_table_supports_json - skip unless @connection.supports_json? @connection.change_table("json_data_type") do |t| t.public_send column_type, "users" end klass.reset_column_information column = klass.columns_hash["users"] assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type + assert_type_match column_type, column.sql_type end def test_schema_dumping - skip unless @connection.supports_json? output = dump_table_schema("json_data_type") assert_match(/t\.#{column_type}\s+"settings"/, output) end @@ -68,26 +66,26 @@ module JSONSharedTestCases end def test_rewrite - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + @connection.execute(insert_statement_per_database('{"k":"v"}')) x = klass.first x.payload = { '"a\'' => "b" } assert x.save! end def test_select - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + @connection.execute(insert_statement_per_database('{"k":"v"}')) x = klass.first assert_equal({ "k" => "v" }, x.payload) end def test_select_multikey - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + @connection.execute(insert_statement_per_database('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')) x = klass.first assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) end def test_null_json - @connection.execute("insert into json_data_type (payload) VALUES(null)") + @connection.execute(insert_statement_per_database("null")) x = klass.first assert_nil(x.payload) end @@ -109,13 +107,13 @@ module JSONSharedTestCases end def test_select_array_json_value - @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]')) x = klass.first assert_equal(["v0", { "k1" => "v1" }], x.payload) end def test_rewrite_array_json_value - @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]')) x = klass.first x.payload = ["v1", { "k2" => "v2" }, "v3"] assert x.save! @@ -154,42 +152,42 @@ module JSONSharedTestCases def test_changes_in_place json = klass.new - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? + assert_predicate json, :changed? + assert_predicate json, :payload_changed? json.save! - assert_not json.changed? + assert_not_predicate json, :changed? json.payload["three"] = "four" - assert json.payload_changed? + assert_predicate json, :payload_changed? json.save! json.reload assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? + assert_not_predicate json, :changed? end def test_changes_in_place_ignores_key_order json = klass.new - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = { "three" => "four", "one" => "two" } json.save! json.reload json.payload = { "three" => "four", "one" => "two" } - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] json.save! json.reload json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] - assert_not json.changed? + assert_not_predicate json, :changed? end def test_changes_in_place_with_ruby_object @@ -197,10 +195,10 @@ module JSONSharedTestCases json = klass.create!(payload: time) json.reload - assert_not json.changed? + assert_not_predicate json, :changed? json.payload = time - assert_not json.changed? + assert_not_predicate json, :changed? end def test_assigning_string_literal @@ -255,4 +253,17 @@ module JSONSharedTestCases def klass JsonDataType end + + def assert_type_match(type, sql_type) + native_type = ActiveRecord::Base.connection.native_database_types[type][:name] + assert_match %r(\A#{native_type}\b), sql_type + end + + def insert_statement_per_database(values) + if current_adapter?(:OracleAdapter) + "insert into json_data_type (id, payload) VALUES (json_data_type_seq.nextval, '#{values}')" + else + "insert into json_data_type (payload) VALUES ('#{values}')" + end + end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index e857180bd1..02be0aa9e3 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -69,8 +69,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StaleObjectError) { s2.destroy } assert s1.destroy - assert s1.frozen? - assert s1.destroyed? + assert_predicate s1, :frozen? + assert_predicate s1, :destroyed? assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") } end @@ -104,8 +104,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::StaleObjectError) { p2.destroy } assert p1.destroy - assert p1.frozen? - assert p1.destroyed? + assert_predicate p1, :frozen? + assert_predicate p1, :destroyed? assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) } end @@ -201,7 +201,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase person.first_name = "Douglas Adams" person.lock_version = 42 - assert person.lock_version_changed? + assert_predicate person, :lock_version_changed? person.save end @@ -399,25 +399,59 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_counter_cache_with_touch_and_lock_version + car = Car.create! + + assert_equal 0, car.wheels_count + assert_equal 0, car.lock_version + + previously_car_updated_at = car.updated_at + travel(2.second) do + Wheel.create!(wheelable: car) + end + + assert_equal 1, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 1, car.lock_version + + previously_car_updated_at = car.updated_at + travel(1.day) do + car.wheels.first.update(size: 42) + end + + assert_equal 1, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 2, car.lock_version + + previously_car_updated_at = car.updated_at + travel(2.second) do + car.wheels.first.destroy! + end + + assert_equal 0, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 3, car.lock_version + end + def test_polymorphic_destroy_with_dependencies_and_lock_version car = Car.create! assert_difference "car.wheels.count" do - car.wheels << Wheel.create! + car.wheels.create end assert_difference "car.wheels.count", -1 do car.reload.destroy end - assert car.destroyed? + assert_predicate car, :destroyed? end def test_removing_has_and_belongs_to_many_associations_upon_destroy p = RichPerson.create! first_name: "Jon" p.treasures.create! - assert !p.treasures.empty? + assert_not_empty p.treasures p.destroy - assert p.treasures.empty? - assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? + assert_empty p.treasures + assert_empty RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1") end def test_yaml_dumping_with_lock_column @@ -487,7 +521,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase t1.destroy - assert t1.destroyed? + assert_predicate t1, :destroyed? end def test_destroy_stale_object @@ -500,7 +534,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase stale_object.destroy! end - refute stale_object.destroyed? + assert_not_predicate stale_object, :destroyed? end private diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 208e54ed0b..e2742ed33e 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -33,8 +33,9 @@ class LogSubscriberTest < ActiveRecord::TestCase super end - def debug(message) - @debugs << message + def debug(progname = nil, &block) + @debugs << progname + super end end @@ -171,6 +172,22 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + def test_vebose_query_logs + ActiveRecord::Base.verbose_query_logs = true + + logger = TestDebugLogSubscriber.new + logger.sql(Event.new(0, sql: "hi mom!")) + assert_match(/↳/, @logger.logged(:debug).last) + ensure + ActiveRecord::Base.verbose_query_logs = false + end + + def test_verbose_query_logs_disabled_by_default + logger = TestDebugLogSubscriber.new + logger.sql(Event.new(0, sql: "hi mom!")) + assert_no_match(/↳/, @logger.logged(:debug).last) + end + def test_cached_queries ActiveRecord::Base.cache do Developer.all.load diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 7b0644e9c0..1494027182 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -84,7 +84,7 @@ module ActiveRecord columns = connection.columns(:testings) array_column = columns.detect { |c| c.name == "foo" } - assert array_column.array? + assert_predicate array_column, :array? end def test_create_table_with_array_column @@ -95,7 +95,7 @@ module ActiveRecord columns = connection.columns(:testings) array_column = columns.detect { |c| c.name == "foo" } - assert array_column.array? + assert_predicate array_column, :array? end end @@ -264,19 +264,18 @@ module ActiveRecord t.column :foo, :timestamp end - klass = Class.new(ActiveRecord::Base) - klass.table_name = "testings" + column = connection.columns(:testings).find { |c| c.name == "foo" } - assert_equal :datetime, klass.columns_hash["foo"].type + assert_equal :datetime, column.type if current_adapter?(:PostgreSQLAdapter) - assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type + assert_equal "timestamp without time zone", column.sql_type elsif current_adapter?(:Mysql2Adapter) - assert_equal "timestamp", klass.columns_hash["foo"].sql_type + assert_equal "timestamp", column.sql_type elsif current_adapter?(:OracleAdapter) - assert_equal "TIMESTAMP(6)", klass.columns_hash["foo"].sql_type + assert_equal "TIMESTAMP(6)", column.sql_type else - assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type + assert_equal connection.type_to_sql("datetime"), column.sql_type end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index be6dc2acb1..3022121f4c 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -80,7 +80,7 @@ module ActiveRecord TestModel.delete_all # Now use the Rails insertion - TestModel.create wealth: BigDecimal.new("12345678901234567890.0123456789") + TestModel.create wealth: BigDecimal("12345678901234567890.0123456789") # SELECT row = TestModel.first @@ -146,7 +146,7 @@ module ActiveRecord TestModel.create first_name: "bob", last_name: "bobsen", bio: "I was born ....", age: 18, height: 1.78, - wealth: BigDecimal.new("12345678901234567890.0123456789"), + wealth: BigDecimal("12345678901234567890.0123456789"), birthday: 18.years.ago, favorite_day: 10.days.ago, moment_of_truth: "1782-10-10 21:40:18", male: true @@ -159,7 +159,7 @@ module ActiveRecord # Test for 30 significant digits (beyond the 16 of float), 10 of them # after the decimal place. - assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth + assert_equal BigDecimal("0012345678901234567890.0123456789"), bob.wealth assert_equal true, bob.male? diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 8ca20b6172..cedd9c44e3 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -67,7 +67,7 @@ module ActiveRecord if current_adapter?(:Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" - assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? + assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment? TestModel.reset_column_information ensure rename_column "test_models", "id_test", "id" @@ -223,31 +223,31 @@ module ActiveRecord def test_change_column_with_nil_default add_column "test_models", "contributor", :boolean, default: true - assert TestModel.new.contributor? + assert_predicate TestModel.new, :contributor? change_column "test_models", "contributor", :boolean, default: nil TestModel.reset_column_information - assert_not TestModel.new.contributor? + assert_not_predicate TestModel.new, :contributor? assert_nil TestModel.new.contributor end def test_change_column_to_drop_default_with_null_false add_column "test_models", "contributor", :boolean, default: true, null: false - assert TestModel.new.contributor? + assert_predicate TestModel.new, :contributor? change_column "test_models", "contributor", :boolean, default: nil, null: false TestModel.reset_column_information - assert_not TestModel.new.contributor? + assert_not_predicate TestModel.new, :contributor? assert_nil TestModel.new.contributor end def test_change_column_with_new_default add_column "test_models", "administrator", :boolean, default: true - assert TestModel.new.administrator? + assert_predicate TestModel.new, :administrator? change_column "test_models", "administrator", :boolean, default: false TestModel.reset_column_information - assert_not TestModel.new.administrator? + assert_not_predicate TestModel.new, :administrator? end def test_change_column_with_custom_index_name diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 58bc558619..3a11bb081b 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -14,7 +14,7 @@ module ActiveRecord recorder = CommandRecorder.new(Class.new { def america; end }.new) - assert recorder.respond_to?(:america) + assert_respond_to recorder, :america end def test_send_calls_super @@ -27,7 +27,7 @@ module ActiveRecord recorder = CommandRecorder.new(Class.new { def create_table(name); end }.new) - assert recorder.respond_to?(:create_table), "respond_to? create_table" + assert_respond_to recorder, :create_table recorder.send(:create_table, :horses) assert_equal [[:create_table, [:horses], nil]], recorder.commands end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 2fef2f796e..69a50674af 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -16,7 +16,7 @@ module ActiveRecord ActiveRecord::Migration.verbose = false connection.create_table :testings do |t| - t.column :foo, :string, limit: 100 + t.column :foo, :string, limit: 5 t.column :bar, :string, limit: 100 end end @@ -126,6 +126,25 @@ module ActiveRecord end assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) end + + if current_adapter?(:PostgreSQLAdapter) + class Testing < ActiveRecord::Base + end + + def test_legacy_change_column_with_null_executes_update + migration = Class.new(ActiveRecord::Migration[5.1]) { + def migrate(x) + change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar" + end + }.new + + Testing.create! + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_equal ["foobar"], Testing.all.map(&:foo) + ensure + ActiveRecord::Base.clear_cache! + end + end end end end @@ -160,12 +179,10 @@ module LegacyPrimaryKeyTestCases @migration.migrate(:up) - legacy_pk = LegacyPrimaryKey.columns_hash["id"] - assert_not legacy_pk.bigint? - assert_not legacy_pk.null + assert_legacy_primary_key legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] - assert_not legacy_ref.bigint? + assert_not_predicate legacy_ref, :bigint? record1 = LegacyPrimaryKey.create! assert_not_nil record1.id @@ -197,55 +214,50 @@ module LegacyPrimaryKeyTestCases assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema end - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) - def test_legacy_primary_key_in_create_table_should_be_integer - @migration = Class.new(migration_class) { - def change - create_table :legacy_primary_keys, id: false do |t| - t.primary_key :id - end + def test_legacy_primary_key_in_create_table_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.primary_key :id end - }.new + end + }.new - @migration.migrate(:up) + @migration.migrate(:up) - schema = dump_table_schema "legacy_primary_keys" - assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema - end + assert_legacy_primary_key + end - def test_legacy_primary_key_in_change_table_should_be_integer - @migration = Class.new(migration_class) { - def change - create_table :legacy_primary_keys, id: false do |t| - t.integer :dummy - end - change_table :legacy_primary_keys do |t| - t.primary_key :id - end + def test_legacy_primary_key_in_change_table_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.integer :dummy end - }.new + change_table :legacy_primary_keys do |t| + t.primary_key :id + end + end + }.new - @migration.migrate(:up) + @migration.migrate(:up) - schema = dump_table_schema "legacy_primary_keys" - assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema - end + assert_legacy_primary_key + end - def test_add_column_with_legacy_primary_key_should_be_integer - @migration = Class.new(migration_class) { - def change - create_table :legacy_primary_keys, id: false do |t| - t.integer :dummy - end - add_column :legacy_primary_keys, :id, :primary_key + def test_add_column_with_legacy_primary_key_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.integer :dummy end - }.new + add_column :legacy_primary_keys, :id, :primary_key + end + }.new - @migration.migrate(:up) + @migration.migrate(:up) - schema = dump_table_schema "legacy_primary_keys" - assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema - end + assert_legacy_primary_key end def test_legacy_join_table_foreign_keys_should_be_integer @@ -289,8 +301,8 @@ module LegacyPrimaryKeyTestCases @migration.migrate(:up) legacy_pk = LegacyPrimaryKey.columns_hash["id"] - assert legacy_pk.bigint? - assert legacy_pk.auto_increment? + assert_predicate legacy_pk, :bigint? + assert_predicate legacy_pk, :auto_increment? schema = dump_table_schema "legacy_primary_keys" assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema @@ -314,6 +326,22 @@ module LegacyPrimaryKeyTestCases assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema end end + + private + def assert_legacy_primary_key + assert_equal "id", LegacyPrimaryKey.primary_key + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + + assert_equal :integer, legacy_pk.type + assert_not_predicate legacy_pk, :bigint? + assert_not legacy_pk.null + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema + end + end end module LegacyPrimaryKeyTest diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 77d32a24a5..83fb4f9385 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -69,7 +69,7 @@ module ActiveRecord def test_create_join_table_without_indexes connection.create_join_table :artists, :musics - assert connection.indexes(:artists_musics).blank? + assert_predicate connection.indexes(:artists_musics), :blank? end def test_create_join_table_with_index diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 499d072de5..de37215e80 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -227,6 +227,74 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end end + if ActiveRecord::Base.connection.supports_validate_constraints? + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_not_predicate fk, :validated? + end + + def test_validate_foreign_key_infers_column + @connection.add_foreign_key :astronauts, :rockets, validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, :rockets + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, column: "rocket_id" + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_key_by_symbol_column + @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, column: :rocket_id + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, name: "fancy_named_fk" + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.validate_foreign_key :astronauts, :rockets + end + end + + def test_validate_constraint_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false + + @connection.validate_constraint :astronauts, "fancy_named_fk" + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + else + # Foreign key should still be created, but should not be invalid + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_predicate fk, :validated? + end + end + def test_schema_dumping @connection.add_foreign_key :astronauts, :rockets output = dump_table_schema "astronauts" diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index d0066f68be..dedb5ea502 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -4,37 +4,37 @@ require "cases/helper" module ActiveRecord class Migration - class PendingMigrationsTest < ActiveRecord::TestCase - def setup - super - @connection = Minitest::Mock.new - @app = Minitest::Mock.new - conn = @connection - @pending = Class.new(CheckPending) { - define_method(:connection) { conn } - }.new(@app) - @pending.instance_variable_set :@last_check, -1 # Force checking - end + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class PendingMigrationsTest < ActiveRecord::TestCase + setup do + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid" + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end - def teardown - assert @connection.verify - assert @app.verify - super - end + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end + + def test_errors_if_pending + ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - def test_errors_if_pending - ActiveRecord::Migrator.stub :needs_migration?, true do - assert_raise ActiveRecord::PendingMigrationError do - @pending.call(nil) + assert_raises ActiveRecord::PendingMigrationError do + CheckPending.new(Proc.new {}).call({}) end end - end - def test_checks_if_supported - @app.expect :call, nil, [:foo] + def test_checks_if_supported + ActiveRecord::SchemaMigration.create_table + migrator = Base.connection.migration_context + capture(:stdout) { migrator.migrate } - ActiveRecord::Migrator.stub :needs_migration?, false do - @pending.call(:foo) + assert_nil CheckPending.new(Proc.new {}).call({}) end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index dfce266253..8514ccd55b 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -86,7 +86,7 @@ module ActiveRecord rename_table :cats, :felines assert connection.table_exists? :felines - refute connection.table_exists? :cats + assert_not connection.table_exists? :cats primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] SELECT c.relname @@ -107,7 +107,7 @@ module ActiveRecord connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } assert connection.table_exists? :felines - refute connection.table_exists? :cats + assert_not connection.table_exists? :cats ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index b18af2ab55..a4b8664a85 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -71,6 +71,16 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end + def test_migrator_migrations_path_is_deprecated + assert_deprecated do + ActiveRecord::Migrator.migrations_path = "/whatever" + end + ensure + assert_deprecated do + ActiveRecord::Migrator.migrations_path = "db/migrate" + end + end + def test_migration_version_matches_component_version assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version end @@ -78,20 +88,20 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - ActiveRecord::Migrator.up(migrations_path) - assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal false, ActiveRecord::Migrator.needs_migration? + migrator.up + assert_equal 3, migrator.current_version + assert_equal false, migrator.needs_migration? - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") - assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal true, ActiveRecord::Migrator.needs_migration? + migrator.down + assert_equal 0, migrator.current_version + assert_equal true, migrator.needs_migration? ActiveRecord::SchemaMigration.create!(version: 3) - assert_equal true, ActiveRecord::Migrator.needs_migration? + assert_equal true, migrator.needs_migration? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_migration_detection_without_schema_migration_table @@ -99,28 +109,31 @@ class MigrationTest < ActiveRecord::TestCase migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - assert_equal true, ActiveRecord::Migrator.needs_migration? + assert_equal true, migrator.needs_migration? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_any_migrations old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid" + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") - assert ActiveRecord::Migrator.any_migrations? + assert_predicate migrator, :any_migrations? - ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty" + migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty") - assert_not ActiveRecord::Migrator.any_migrations? + assert_not_predicate migrator_empty, :any_migrations? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_migration_version - assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) } + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check") + assert_equal 0, migrator.current_version + migrator.up(20131219224947) + assert_equal 20131219224947, migrator.current_version end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -157,7 +170,7 @@ class MigrationTest < ActiveRecord::TestCase def test_add_table_with_decimals Person.connection.drop_table :big_numbers rescue nil - assert !BigNumber.table_exists? + assert_not_predicate BigNumber, :table_exists? GiveMeBigNumbers.up BigNumber.reset_column_information @@ -216,15 +229,16 @@ class MigrationTest < ActiveRecord::TestCase def test_filtering_migrations assert_no_column Person, :last_name - assert !Reminder.table_exists? + assert_not_predicate Reminder, :table_exists? name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") + migrator.up(&name_filter) assert_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) + migrator.down(&name_filter) assert_no_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } @@ -382,9 +396,9 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - ActiveRecord::Migrator.up(migrations_path) + migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] original_rails_env = ENV["RAILS_ENV"] @@ -395,13 +409,13 @@ class MigrationTest < ActiveRecord::TestCase refute_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database - ActiveRecord::Migrator.up(migrations_path) + migrator.up assert_equal new_env, ActiveRecord::InternalMetadata[:environment] ensure - ActiveRecord::Migrator.migrations_paths = old_path + migrator = ActiveRecord::MigrationContext.new(old_path) ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env - ActiveRecord::Migrator.up(migrations_path) + migrator.up end def test_internal_metadata_stores_environment_when_other_data_exists @@ -411,14 +425,15 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - ActiveRecord::Migrator.up(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] ensure - ActiveRecord::Migrator.migrations_paths = old_path + migrator = ActiveRecord::MigrationContext.new(old_path) + migrator.up end def test_proper_table_name_on_migration @@ -450,7 +465,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_rename_table_with_prefix_and_suffix - assert !Thing.table_exists? + assert_not_predicate Thing, :table_exists? ActiveRecord::Base.table_name_prefix = "p_" ActiveRecord::Base.table_name_suffix = "_s" Thing.reset_table_name @@ -471,7 +486,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_add_drop_table_with_prefix_and_suffix - assert !Reminder.table_exists? + assert_not_predicate Reminder, :table_exists? ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name @@ -801,8 +816,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.integer :age end - # Adding an index fires a query every time to check if an index already exists or not - assert_queries(3) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not + "PostgreSQLAdapter" => 2, + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.index :username, unique: true, name: :awesome_username_index t.index [:name, :age] @@ -826,7 +848,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert index(:index_delete_me_on_name) - assert_queries(3) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not + "PostgreSQLAdapter" => 2, + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.remove_index :name t.index :name, name: :new_name_index, unique: true @@ -848,18 +878,24 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert ! column(:name).default assert_equal :date, column(:birthdate).type - # One query for columns (delete_me table) - # One query for primary key (delete_me table) - # One query to do the bulk change - assert_queries(3, ignore_none: true) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change + "PostgreSQLAdapter" => 3, # one query for columns, one for bulk change, one for comment + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count, ignore_none: true) do with_bulk_change_table do |t| t.change :name, :string, default: "NONAME" - t.change :birthdate, :datetime + t.change :birthdate, :datetime, comment: "This is a comment" end end assert_equal "NONAME", column(:name).default assert_equal :datetime, column(:birthdate).type + assert_equal "This is a comment", column(:birthdate).comment end private @@ -919,7 +955,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied ensure clear end @@ -960,7 +996,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied end ensure clear @@ -1002,7 +1038,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied end ensure clear @@ -1023,7 +1059,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic") assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? + assert_empty copied ensure clear end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1047ba1367..873455cf67 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -89,7 +89,7 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid") + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -98,7 +98,8 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_subdirectories - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_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 @@ -108,7 +109,7 @@ class MigratorTest < ActiveRecord::TestCase def test_finds_migrations_from_two_directories directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] - migrations = ActiveRecord::Migrator.migrations directories + migrations = ActiveRecord::MigrationContext.new(directories).migrations [[20090101010101, "PeopleHaveHobbies"], [20090101010202, "PeopleHaveDescriptions"], @@ -121,14 +122,14 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_numbered_directory - migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + "/10_urban"] + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations assert_equal 9, migrations[0].version assert_equal "AddExpressions", migrations[0].name end def test_relative_migrations list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.migrations("valid") + ActiveRecord::MigrationContext.new("valid").migrations end migration_proxy = list.find { |item| @@ -157,7 +158,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status end def test_migrations_status_in_subdirectories @@ -171,7 +172,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status end def test_migrations_status_with_schema_define_in_subdirectories @@ -186,7 +187,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "001", "Valid people have last names"], ["up", "002", "We need reminders"], ["up", "003", "Innocent jointable"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status ensure ActiveRecord::Migrator.migrations_paths = prev_paths end @@ -204,7 +205,7 @@ class MigratorTest < ActiveRecord::TestCase ["down", "20100201010101", "Valid with timestamps we need reminders"], ["down", "20100301010101", "Valid with timestamps innocent jointable"], ["up", "20160528010101", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(paths) + ], ActiveRecord::MigrationContext.new(paths).migrations_status end def test_migrator_interleaved_migrations @@ -232,25 +233,28 @@ class MigratorTest < ActiveRecord::TestCase def test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:up, migrations).migrate + migrator = ActiveRecord::Migrator.new(:up, migrations) + migrator.migrate assert migrations.all?(&:went_up) assert migrations.all? { |m| !m.went_down } - assert_equal 2, ActiveRecord::Migrator.current_version + assert_equal 2, migrator.current_version end def test_down_calls_down test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:down, migrations).migrate + migrator = ActiveRecord::Migrator.new(:down, migrations) + migrator.migrate assert migrations.all? { |m| !m.went_up } assert migrations.all?(&:went_down) - assert_equal 0, ActiveRecord::Migrator.current_version + assert_equal 0, migrator.current_version end def test_current_version ActiveRecord::SchemaMigration.create!(version: "1000") - assert_equal 1000, ActiveRecord::Migrator.current_version + migrator = ActiveRecord::MigrationContext.new("db/migrate") + assert_equal 1000, migrator.current_version end def test_migrator_one_up @@ -289,33 +293,36 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_double_up calls, migrations = sensors(3) - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator = ActiveRecord::Migrator.new(:up, migrations, 1) + assert_equal(0, migrator.current_version) - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + migrator.migrate assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + migrator.migrate assert_equal [], calls end def test_migrator_double_down calls, migrations = sensors(3) + migrator = ActiveRecord::Migrator.new(:up, migrations, 1) - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal 0, migrator.current_version - ActiveRecord::Migrator.new(:up, migrations, 1).run + migrator.run assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run + migrator = ActiveRecord::Migrator.new(:down, migrations, 1) + migrator.run assert_equal [[:down, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run + migrator.run assert_equal [], calls - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal 0, migrator.current_version end def test_migrator_verbosity @@ -361,78 +368,85 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_going_down_due_to_version_target calls, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.up("valid", 1) + migrator.up(1) assert_equal [[:up, 1]], calls calls.clear - migrator.migrate("valid", 0) + migrator.migrate(0) assert_equal [[:down, 1]], calls calls.clear - migrator.migrate("valid") + migrator.migrate assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls end def test_migrator_output_when_running_multiple_migrations _, migrator = migrator_class(3) + migrator = migrator.new("valid") - result = migrator.migrate("valid") + result = migrator.migrate assert_equal(3, result.count) # Nothing migrated from duplicate run - result = migrator.migrate("valid") + result = migrator.migrate assert_equal(0, result.count) - result = migrator.rollback("valid") + result = migrator.rollback assert_equal(1, result.count) end def test_migrator_output_when_running_single_migration _, migrator = migrator_class(1) - result = migrator.run(:up, "valid", 1) + migrator = migrator.new("valid") + + result = migrator.run(:up, 1) assert_equal(1, result.version) end def test_migrator_rollback _, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.migrate("valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.migrate + assert_equal(3, migrator.current_version) - migrator.rollback("valid") - assert_equal(2, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(2, migrator.current_version) - migrator.rollback("valid") - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(1, migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(0, migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(0, migrator.current_version) end def test_migrator_db_has_no_schema_migrations_table _, migrator = migrator_class(3) + migrator = migrator.new("valid") ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") - migrator.migrate("valid", 1) + migrator.migrate(1) assert ActiveRecord::Base.connection.table_exists?("schema_migrations") end def test_migrator_forward _, migrator = migrator_class(3) - migrator.migrate("/valid", 1) - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator = migrator.new("/valid") + migrator.migrate(1) + assert_equal(1, migrator.current_version) - migrator.forward("/valid", 2) - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.forward(2) + assert_equal(3, migrator.current_version) - migrator.forward("/valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.forward + assert_equal(3, migrator.current_version) end def test_only_loads_pending_migrations @@ -440,25 +454,27 @@ class MigratorTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.create!(version: "1") calls, migrator = migrator_class(3) - migrator.migrate("valid", nil) + migrator = migrator.new("valid") + migrator.migrate assert_equal [[:up, 2], [:up, 3]], calls end def test_get_all_versions _, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.migrate("valid") - assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions) + migrator.migrate + assert_equal([1, 2, 3], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([1, 2], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([1], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([], migrator.get_all_versions) end private @@ -483,11 +499,11 @@ class MigratorTest < ActiveRecord::TestCase def migrator_class(count) calls, migrations = sensors(count) - migrator = Class.new(ActiveRecord::Migrator).extend(Module.new { - define_method(:migrations) { |paths| + migrator = Class.new(ActiveRecord::MigrationContext) { + define_method(:migrations) { |*| migrations } - }) + } [calls, migrator] end end diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index 59be4dc5a8..a24b173cf5 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -227,7 +227,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + assert_not_respond_to topic.written_on, :time_zone end end @@ -242,7 +242,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + assert_not_respond_to topic.written_on, :time_zone end ensure Topic.skip_time_zone_conversion_for_attributes = [] @@ -261,7 +261,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert_not topic.bonus_time.utc? + assert_not_predicate topic.bonus_time, :utc? attributes = { "written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "", diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index a2ccb603a9..1ed3a61bbb 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -36,7 +36,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "", _destroy: "0" }] pirate.save! - assert pirate.birds_with_reject_all_blank.empty? + assert_empty pirate.birds_with_reject_all_blank end def test_should_not_build_a_new_record_if_reject_all_blank_returns_false @@ -44,7 +44,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "" }] pirate.save! - assert pirate.birds_with_reject_all_blank.empty? + assert_empty pirate.birds_with_reject_all_blank end def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false @@ -152,7 +152,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase man = Man.create(name: "Jon") interest = man.interests.create(topic: "the ladies") man.update(interests_attributes: { _destroy: "1", id: interest.id }) - assert man.reload.interests.empty? + assert_empty man.reload.interests end def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false @@ -240,7 +240,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase @ship.destroy @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" } - assert !@pirate.ship.persisted? + assert_not_predicate @pirate.ship, :persisted? assert_equal "Davy Jones Gold Dagger", @pirate.ship.name end @@ -261,7 +261,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_replace_an_existing_record_if_there_is_no_id @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" } - assert !@pirate.ship.persisted? + assert_not_predicate @pirate.ship, :persisted? assert_equal "Davy Jones Gold Dagger", @pirate.ship.name assert_equal "Nights Dirty Lightning", @ship.name end @@ -335,7 +335,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_also_work_with_a_HashWithIndifferentAccess @pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(id: @ship.id, name: "Davy Jones Gold Dagger") - assert @pirate.ship.persisted? + assert_predicate @pirate.ship, :persisted? assert_equal "Davy Jones Gold Dagger", @pirate.ship.name end @@ -350,12 +350,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_destroy_the_associated_model_until_the_parent_is_saved @pirate.attributes = { ship_attributes: { id: @ship.id, _destroy: "1" } } - assert !@pirate.ship.destroyed? - assert @pirate.ship.marked_for_destruction? + assert_not_predicate @pirate.ship, :destroyed? + assert_predicate @pirate.ship, :marked_for_destruction? @pirate.save - assert @pirate.ship.destroyed? + assert_predicate @pirate.ship, :destroyed? assert_nil @pirate.reload.ship end @@ -424,7 +424,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @pirate.destroy @ship.reload.pirate_attributes = { catchphrase: "Arr" } - assert !@ship.pirate.persisted? + assert_not_predicate @ship.pirate, :persisted? assert_equal "Arr", @ship.pirate.catchphrase end @@ -445,7 +445,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_replace_an_existing_record_if_there_is_no_id @ship.reload.pirate_attributes = { catchphrase: "Arr" } - assert !@ship.pirate.persisted? + assert_not_predicate @ship.pirate, :persisted? assert_equal "Arr", @ship.pirate.catchphrase assert_equal "Aye", @pirate.catchphrase end @@ -550,7 +550,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @pirate.delete @ship.reload.attributes = { update_only_pirate_attributes: { catchphrase: "Arr" } } - assert !@ship.update_only_pirate.persisted? + assert_not_predicate @ship.update_only_pirate, :persisted? end def test_should_update_existing_when_update_only_is_true_and_no_id_is_given @@ -632,10 +632,10 @@ module NestedAttributesOnACollectionAssociationTests def test_should_not_load_association_when_updating_existing_records @pirate.reload @pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }]) - assert ! @pirate.send(@association_name).loaded? + assert_not_predicate @pirate.send(@association_name), :loaded? @pirate.save - assert ! @pirate.send(@association_name).loaded? + assert_not_predicate @pirate.send(@association_name), :loaded? assert_equal "Grace OMalley", @child_1.reload.name end @@ -663,7 +663,7 @@ module NestedAttributesOnACollectionAssociationTests def test_should_not_remove_scheduled_destroys_when_loading_association @pirate.reload @pirate.send(association_setter, [{ id: @child_1.id, _destroy: "1" }]) - assert @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.marked_for_destruction? + assert_predicate @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }, :marked_for_destruction? end def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models @@ -705,10 +705,10 @@ module NestedAttributesOnACollectionAssociationTests association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" } } } - assert !@pirate.send(@association_name).first.persisted? + assert_not_predicate @pirate.send(@association_name).first, :persisted? assert_equal "Grace OMalley", @pirate.send(@association_name).first.name - assert !@pirate.send(@association_name).last.persisted? + assert_not_predicate @pirate.send(@association_name).last, :persisted? assert_equal "Privateers Greed", @pirate.send(@association_name).last.name end @@ -1091,7 +1091,7 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR test "nested singular associations are validated" do part = ShipPart.new(name: "Stern", ship_attributes: { name: nil }) - assert_not part.valid? + assert_not_predicate part, :valid? assert_equal ["Ship name can't be blank"], part.errors.full_messages end end diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index f04c68b08f..1d26057fdc 100644 --- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -63,7 +63,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase # Characterizing when :before_add callback is called test ":before_add called for new bird when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = new_bird_attributes assert_new_bird_with_callback_called end @@ -80,7 +80,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase end test ":before_add not called for identical assignment when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = existing_birds_attributes assert_callbacks_not_called end @@ -92,7 +92,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase end test ":before_add not called for destroy assignment when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = destroy_bird_attributes assert_callbacks_not_called end @@ -111,7 +111,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase # Ensuring that the records in the association target are updated, # whether the association is loaded before or not test "Assignment updates records in target when not loaded" do - assert_not @pirate.birds_with_add.loaded? + assert_not_predicate @pirate.birds_with_add, :loaded? @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes assert_assignment_affects_records_in_target(:birds_with_add) end @@ -124,7 +124,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase test("Assignment updates records in target when not loaded" \ " and callback loads target") do - assert_not @pirate.birds_with_add_load.loaded? + assert_not_predicate @pirate.birds_with_add_load, :loaded? @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes assert_assignment_affects_records_in_target(:birds_with_add_load) end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index f088c064f5..d4a4cd564a 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -59,18 +59,47 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_all_with_order_and_limit_updates_subset_only author = authors(:david) - assert_nothing_raised do - assert_equal 1, author.posts_sorted_by_id_limited.size - assert_equal 2, author.posts_sorted_by_id_limited.limit(2).to_a.size - assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:welcome).body - assert_not_equal "bulk update!", posts(:thinking).body - end + 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 def test_update_many - topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, nil => {} } + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } updated = Topic.update(topic_data.keys, topic_data.values) assert_equal [1, 2], updated.map(&:id) @@ -78,10 +107,33 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "2 updated", Topic.find(2).content end + def test_update_many_with_duplicated_ids + updated = Topic.update([1, 1, 2], [ + { "content" => "1 duplicated" }, { "content" => "1 updated" }, { "content" => "2 updated" } + ]) + + assert_equal [1, 1, 2], updated.map(&:id) + assert_equal "1 updated", Topic.find(1).content + assert_equal "2 updated", Topic.find(2).content + end + + def test_update_many_with_invalid_id + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} } + + assert_raise(ActiveRecord::RecordNotFound) do + Topic.update(topic_data.keys, topic_data.values) + end + + assert_not_equal "1 updated", Topic.find(1).content + assert_not_equal "2 updated", Topic.find(2).content + end + def test_class_level_update_is_affected_by_scoping topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } - assert_equal [], Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + end assert_not_equal "1 updated", Topic.find(1).content assert_not_equal "2 updated", Topic.find(2).content @@ -165,7 +217,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a - assert ! topics_by_mary.empty? + assert_not_empty topics_by_mary assert_difference("Topic.count", -topics_by_mary.size) do destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) @@ -175,23 +227,40 @@ class PersistenceTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.all.merge!(order: "id").find([2, 3]) + clients = Client.find([2, 3]) assert_difference("Client.count", -2) do - destroyed = Client.destroy([2, 3, nil]).sort_by(&:id) + destroyed = Client.destroy([2, 3]) assert_equal clients, destroyed assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" end end + def test_destroy_many_with_invalid_id + clients = Client.find([2, 3]) + + assert_raise(ActiveRecord::RecordNotFound) do + Client.destroy([2, 3, 99999]) + end + + assert_equal clients, Client.find([2, 3]) + end + def test_becomes assert_kind_of Reply, topics(:first).becomes(Reply) assert_equal "The First Topic", topics(:first).becomes(Reply).title end + def test_becomes_after_reload_schema_from_cache + Reply.define_attribute_methods + Reply.serialize(:content) # invoke reload_schema_from_cache + assert_kind_of Reply, topics(:first).becomes(Reply) + assert_equal "The First Topic", topics(:first).becomes(Reply).title + end + def test_becomes_includes_errors company = Company.new(name: nil) - assert !company.valid? + assert_not_predicate company, :valid? original_errors = company.errors client = company.becomes(Client) assert_equal original_errors.keys, client.errors.keys @@ -308,8 +377,8 @@ class PersistenceTest < ActiveRecord::TestCase developer.destroy new_developer = developer.dup new_developer.save - assert new_developer.persisted? - assert_not new_developer.destroyed? + assert_predicate new_developer, :persisted? + assert_not_predicate new_developer, :destroyed? end def test_create_many @@ -320,12 +389,13 @@ class PersistenceTest < ActiveRecord::TestCase def test_create_columns_not_equal_attributes topic = Topic.instantiate( - "attributes" => { - "title" => "Another New Topic", - "does_not_exist" => "test" - } + "title" => "Another New Topic", + "does_not_exist" => "test" ) + topic = topic.dup # reset @new_record assert_nothing_raised { topic.save } + assert_predicate topic, :persisted? + assert_equal "Another New Topic", topic.reload.title end def test_create_through_factory_with_block @@ -372,6 +442,8 @@ class PersistenceTest < ActiveRecord::TestCase topic_reloaded = Topic.instantiate(topic.attributes.merge("does_not_exist" => "test")) topic_reloaded.title = "A New Topic" assert_nothing_raised { topic_reloaded.save } + assert_predicate topic_reloaded, :persisted? + assert_equal "A New Topic", topic_reloaded.reload.title end def test_update_for_record_with_only_primary_key @@ -408,6 +480,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_instance_of Reply, Reply.find(reply.id) end + def test_becomes_default_sti_subclass + original_type = Topic.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :topics, :type, "Reply" + Topic.reset_column_information + + reply = topics(:second) + assert_instance_of Reply, reply + + topic = reply.becomes(Topic) + assert_instance_of Topic, topic + + ensure + ActiveRecord::Base.connection.change_column_default :topics, :type, original_type + Topic.reset_column_information + end + def test_update_after_create klass = Class.new(Topic) do def self.name; "Topic"; end @@ -473,10 +561,18 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } end - def test_record_not_found_exception + def test_find_raises_record_not_found_exception assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end + def test_update_raises_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { Topic.update(99999, approved: true) } + end + + def test_destroy_raises_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { Topic.destroy(99999) } + end + def test_update_all assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'") assert_equal "bulk updated!", Topic.find(1).content @@ -526,40 +622,65 @@ class PersistenceTest < ActiveRecord::TestCase end def test_delete_new_record - client = Client.new + client = Client.new(name: "37signals") client.delete - assert client.frozen? + assert_predicate client, :frozen? + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? + assert_raise(RuntimeError) { client.name = "something else" } end def test_delete_record_with_associations client = Client.find(3) client.delete - assert client.frozen? + assert_predicate client, :frozen? assert_kind_of Firm, client.firm + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? assert_raise(RuntimeError) { client.name = "something else" } end def test_destroy_new_record - client = Client.new + client = Client.new(name: "37signals") client.destroy - assert client.frozen? + assert_predicate client, :frozen? + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? + assert_raise(RuntimeError) { client.name = "something else" } end def test_destroy_record_with_associations client = Client.find(3) client.destroy - assert client.frozen? + assert_predicate client, :frozen? assert_kind_of Firm, client.firm + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert_predicate client, :frozen? assert_raise(RuntimeError) { client.name = "something else" } end def test_update_attribute - assert !Topic.find(1).approved? + assert_not_predicate Topic.find(1), :approved? Topic.find(1).update_attribute("approved", true) - assert Topic.find(1).approved? + assert_predicate Topic.find(1), :approved? Topic.find(1).update_attribute(:approved, false) - assert !Topic.find(1).approved? + assert_not_predicate Topic.find(1), :approved? + + Topic.find(1).update_attribute(:change_approved_before_save, true) + assert_predicate Topic.find(1), :approved? end def test_update_attribute_for_readonly_attribute @@ -596,14 +717,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column topic = Topic.find(1) topic.update_column("approved", true) - assert topic.approved? + assert_predicate topic, :approved? topic.reload - assert topic.approved? + assert_predicate topic, :approved? topic.update_column(:approved, false) - assert !topic.approved? + assert_not_predicate topic, :approved? topic.reload - assert !topic.approved? + assert_not_predicate topic, :approved? end def test_update_column_should_not_use_setter_method @@ -690,10 +811,10 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns topic = Topic.find(1) topic.update_columns("approved" => true, title: "Sebastian Topic") - assert topic.approved? + assert_predicate topic, :approved? assert_equal "Sebastian Topic", topic.title topic.reload - assert topic.approved? + assert_predicate topic, :approved? assert_equal "Sebastian Topic", topic.title end @@ -801,33 +922,33 @@ class PersistenceTest < ActiveRecord::TestCase def test_update topic = Topic.find(1) - assert !topic.approved? + assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title topic.update("approved" => true, "title" => "The First Topic Updated") topic.reload - assert topic.approved? + assert_predicate topic, :approved? assert_equal "The First Topic Updated", topic.title topic.update(approved: false, title: "The First Topic") topic.reload - assert !topic.approved? + assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title end def test_update_attributes topic = Topic.find(1) - assert !topic.approved? + assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title topic.update_attributes("approved" => true, "title" => "The First Topic Updated") topic.reload - assert topic.approved? + assert_predicate topic, :approved? assert_equal "The First Topic Updated", topic.title topic.update_attributes(approved: false, title: "The First Topic") topic.reload - assert !topic.approved? + assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do @@ -928,7 +1049,7 @@ class PersistenceTest < ActiveRecord::TestCase Topic.find(1).replies << should_be_destroyed_reply topic = Topic.destroy(1) - assert topic.destroyed? + assert_predicate topic, :destroyed? assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) } @@ -938,7 +1059,9 @@ class PersistenceTest < ActiveRecord::TestCase should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") Topic.find(1).replies << should_not_be_destroyed_reply - assert_nil Topic.where("1=0").scoping { Topic.destroy(1) } + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.destroy(1) } + end assert_nothing_raised { Topic.find(1) } assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) } @@ -1006,13 +1129,13 @@ class PersistenceTest < ActiveRecord::TestCase def test_find_via_reload post = Post.new - assert post.new_record? + assert_predicate post, :new_record? post.id = 1 post.reload assert_equal "Welcome to the weblog", post.title - assert_not post.new_record? + assert_not_predicate post, :new_record? end def test_reload_via_querycache @@ -1060,13 +1183,18 @@ class PersistenceTest < ActiveRecord::TestCase end def test_reset_column_information_resets_children - child = Class.new(Topic) - child.new # force schema to load + child_class = Class.new(Topic) + child_class.new # force schema to load ActiveRecord::Base.connection.add_column(:topics, :foo, :string) Topic.reset_column_information - assert_equal "bar", child.new(foo: :bar).foo + # this should redefine attribute methods + child_class.new + + assert child_class.instance_methods.include?(:foo) + assert child_class.instance_methods.include?(:foo_changed?) + assert_equal "bar", child_class.new(foo: :bar).foo ensure ActiveRecord::Base.connection.remove_column(:topics, :foo) Topic.reset_column_information diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 7f6c2382ca..60dac91ec9 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -207,13 +207,13 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_serial_with_quoted_sequence_name column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function - assert column.serial? + assert_predicate column, :serial? end def test_serial_with_unquoted_sequence_name column = Topic.columns_hash[Topic.primary_key] assert_equal "nextval('topics_id_seq'::regclass)", column.default_function - assert column.serial? + assert_predicate column, :serial? end end end @@ -298,6 +298,8 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase assert_not column.null assert_equal :string, column.type assert_equal 42, column.limit + ensure + Barcode.reset_column_information end test "schema dump primary key includes type and options" do @@ -428,7 +430,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) @connection.create_table(:widgets, id: @pk_type, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } assert_equal :integer, column.type - assert_not column.bigint? + assert_not_predicate column, :bigint? end test "primary key with serial/integer are automatically numbered" do @@ -447,10 +449,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) test "primary key column type with options" do @connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } - assert column.auto_increment? + assert_predicate column, :auto_increment? assert_equal :integer, column.type - assert_not column.bigint? - assert column.unsigned? + assert_not_predicate column, :bigint? + assert_predicate column, :unsigned? schema = dump_table_schema "widgets" assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema @@ -459,10 +461,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) test "bigint primary key with unsigned" do @connection.create_table(:widgets, id: :bigint, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } - assert column.auto_increment? + assert_predicate column, :auto_increment? assert_equal :integer, column.type - assert column.bigint? - assert column.unsigned? + assert_predicate column, :bigint? + assert_predicate column, :unsigned? schema = dump_table_schema "widgets" assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 5cb537b623..8d37f5ea95 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -82,7 +82,7 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off, conn end - assert !ActiveRecord::Base.connection.nil? + assert_not_predicate ActiveRecord::Base.connection, :nil? assert_cache :off middleware { @@ -283,7 +283,7 @@ class QueryCacheTest < ActiveRecord::TestCase payload[:sql].downcase! end - assert_raises RuntimeError do + assert_raises frozen_error_class do ActiveRecord::Base.cache do assert_queries(1) { Task.find(1); Task.find(1) } end @@ -327,7 +327,7 @@ class QueryCacheTest < ActiveRecord::TestCase conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") ActiveRecord::Base.connection_handler.establish_connection(conf) Task.connection_specification_name = "test2" - refute Task.connected? + assert_not_predicate Task, :connected? Task.cache do begin @@ -414,20 +414,20 @@ class QueryCacheTest < ActiveRecord::TestCase def test_query_cache_does_not_establish_connection_if_unconnected with_temporary_connection_pool do ActiveRecord::Base.clear_active_connections! - refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check middleware { - refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" + assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" }.call({}) - refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" + assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" end end def test_query_cache_is_enabled_on_connections_established_after_middleware_runs with_temporary_connection_pool do ActiveRecord::Base.clear_active_connections! - refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check middleware { assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" @@ -444,14 +444,23 @@ class QueryCacheTest < ActiveRecord::TestCase assert ActiveRecord::Base.connection.query_cache_enabled Thread.new { - refute ActiveRecord::Base.connection_pool.query_cache_enabled - refute ActiveRecord::Base.connection.query_cache_enabled + assert_not ActiveRecord::Base.connection_pool.query_cache_enabled + assert_not ActiveRecord::Base.connection.query_cache_enabled }.join }.call({}) end end + def test_query_cache_is_enabled_on_all_connection_pools + middleware { + ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| + assert pool.query_cache_enabled + assert pool.connection.query_cache_enabled + end + }.call({}) + end + private def middleware(&app) executor = Class.new(ActiveSupport::Executor) @@ -553,7 +562,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do ActiveRecord::Base.cache do p = Post.find(1) - assert p.categories.any? + assert_predicate p.categories, :any? p.categories.delete_all end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 897d252cf8..6534770c57 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -111,7 +111,7 @@ module ActiveRecord end def test_quote_bigdecimal - bigdec = BigDecimal.new((1 << 100).to_s) + bigdec = BigDecimal((1 << 100).to_s) assert_equal bigdec.to_s("F"), @quoter.quote(bigdec) end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index d1b85cb4ef..383e43ed55 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -16,10 +16,10 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_cant_save_readonly_record dev = Developer.find(1) - assert !dev.readonly? + assert_not_predicate dev, :readonly? dev.readonly! - assert dev.readonly? + assert_predicate dev, :readonly? assert_nothing_raised do dev.name = "Luscious forbidden fruit." @@ -54,7 +54,7 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_has_many_find_readonly post = Post.find(1) - assert !post.comments.empty? + assert_not_empty post.comments assert !post.comments.any?(&:readonly?) assert !post.comments.to_a.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) @@ -66,44 +66,44 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id - assert !posts(:welcome).people.find(1).readonly? + assert_not_predicate posts(:welcome).people.find(1), :readonly? end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_first - assert !posts(:welcome).people.first.readonly? + assert_not_predicate posts(:welcome).people.first, :readonly? end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_last - assert !posts(:welcome).people.last.readonly? + assert_not_predicate posts(:welcome).people.last, :readonly? end def test_readonly_scoping Post.where("1=1").scoping do - assert !Post.find(1).readonly? - assert Post.readonly(true).find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_not_predicate Post.find(1), :readonly? + assert_predicate Post.readonly(true).find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end Post.joins(" ").scoping do - assert !Post.find(1).readonly? - assert Post.readonly.find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_not_predicate Post.find(1), :readonly? + assert_predicate Post.readonly.find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end # Oracle barfs on this because the join includes unqualified and # conflicting column names unless current_adapter?(:OracleAdapter) Post.joins(", developers").scoping do - assert_not Post.find(1).readonly? - assert Post.readonly.find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_not_predicate Post.find(1), :readonly? + assert_predicate Post.readonly.find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end end Post.readonly(true).scoping do - assert Post.find(1).readonly? - assert Post.readonly.find(1).readonly? - assert !Post.readonly(false).find(1).readonly? + assert_predicate Post.find(1), :readonly? + assert_predicate Post.readonly.find(1), :readonly? + assert_not_predicate Post.readonly(false).find(1), :readonly? end end @@ -111,10 +111,10 @@ class ReadOnlyTest < ActiveRecord::TestCase developer = Developer.find(1) project = Post.find(1) - assert !developer.projects.all_as_method.first.readonly? - assert !developer.projects.all_as_scope.first.readonly? + assert_not_predicate developer.projects.all_as_method.first, :readonly? + assert_not_predicate developer.projects.all_as_scope.first, :readonly? - assert !project.comments.all_as_method.first.readonly? - assert !project.comments.all_as_scope.first.readonly? + assert_not_predicate project.comments.all_as_method.first, :readonly? + assert_not_predicate project.comments.all_as_scope.first, :readonly? end end diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 49170abe6f..61cb0f130d 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -18,6 +18,7 @@ module ActiveRecord class FakePool attr_reader :reaped + attr_reader :flushed def initialize @reaped = false @@ -26,6 +27,10 @@ module ActiveRecord def reap @reaped = true end + + def flush + @flushed = true + end end # A reaper with nil time should never reap connections @@ -47,6 +52,7 @@ module ActiveRecord Thread.pass end assert fp.reaped + assert fp.flushed end def test_pool_has_reaper @@ -73,14 +79,14 @@ module ActiveRecord end Thread.pass while conn.nil? - assert conn.in_use? + assert_predicate conn, :in_use? child.terminate while conn.in_use? Thread.pass end - assert !conn.in_use? + assert_not_predicate conn, :in_use? end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 37c2235f1a..ed19192ad9 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -66,6 +66,9 @@ class ReflectionTest < ActiveRecord::TestCase def test_column_string_type_and_limit assert_equal :string, @first.column_for_attribute("title").type + assert_equal :string, @first.column_for_attribute(:title).type + assert_equal :string, @first.type_for_attribute("title").type + assert_equal :string, @first.type_for_attribute(:title).type assert_equal 250, @first.column_for_attribute("title").limit end @@ -81,6 +84,9 @@ class ReflectionTest < ActiveRecord::TestCase def test_integer_columns assert_equal :integer, @first.column_for_attribute("id").type + assert_equal :integer, @first.column_for_attribute(:id).type + assert_equal :integer, @first.type_for_attribute("id").type + assert_equal :integer, @first.type_for_attribute(:id).type end def test_non_existent_columns_return_null_object @@ -89,6 +95,9 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "attribute_that_doesnt_exist", column.name assert_nil column.sql_type assert_nil column.type + + column = @first.column_for_attribute(:attribute_that_doesnt_exist) + assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column end def test_non_existent_types_are_identity_types @@ -98,6 +107,11 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal object, type.deserialize(object) assert_equal object, type.cast(object) assert_equal object, type.serialize(object) + + type = @first.type_for_attribute(:attribute_that_doesnt_exist) + assert_equal object, type.deserialize(object) + assert_equal object, type.cast(object) + assert_equal object, type.serialize(object) end def test_reflection_klass_for_nested_class_name @@ -149,7 +163,7 @@ class ReflectionTest < ActiveRecord::TestCase expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] } received = Pirate.reflect_on_all_autosave_associations - assert !received.empty? + assert_not_empty received assert_not_equal Pirate.reflect_on_all_associations.length, received.length assert_equal expected, received end @@ -254,23 +268,34 @@ class ReflectionTest < ActiveRecord::TestCase end def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case - @hotel = Hotel.create! - @department = @hotel.departments.create! - @department.chefs.create!(employable: CakeDesigner.create!) - @department.chefs.create!(employable: DrinkDesigner.create!) + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create!(employable: CakeDesigner.create!) + department.chefs.create!(employable: DrinkDesigner.create!) - assert_equal 1, @hotel.cake_designers.size - assert_equal 1, @hotel.drink_designers.size - assert_equal 2, @hotel.chefs.size + assert_equal 1, hotel.cake_designers.size + assert_equal 1, hotel.cake_designers.count + assert_equal 1, hotel.drink_designers.size + assert_equal 1, hotel.drink_designers.count + assert_equal 2, hotel.chefs.size + assert_equal 2, hotel.chefs.count end def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti - @hotel = Hotel.create! - @hotel.mocktail_designers << MocktailDesigner.create! + hotel = Hotel.create! + hotel.mocktail_designers << MocktailDesigner.create! + + assert_equal 1, hotel.mocktail_designers.size + assert_equal 1, hotel.mocktail_designers.count + assert_equal 1, hotel.chef_lists.size + assert_equal 1, hotel.chef_lists.count + + hotel.mocktail_designers = [] - assert_equal 1, @hotel.mocktail_designers.size - assert_equal 1, @hotel.mocktail_designers.count - assert_equal 1, @hotel.chef_lists.size + assert_equal 0, hotel.mocktail_designers.size + assert_equal 0, hotel.mocktail_designers.count + assert_equal 0, hotel.chef_lists.size + assert_equal 0, hotel.chef_lists.count end def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations @@ -290,12 +315,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_nested? - assert !Author.reflect_on_association(:comments).nested? - assert Author.reflect_on_association(:tags).nested? + assert_not_predicate Author.reflect_on_association(:comments), :nested? + assert_predicate Author.reflect_on_association(:tags), :nested? # Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is # a nested through association - assert Category.reflect_on_association(:post_comments).nested? + assert_predicate Category.reflect_on_association(:post_comments), :nested? end def test_association_primary_key @@ -310,15 +335,6 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end - def test_association_primary_key_type - # Normal Association - assert_equal :integer, Author.reflect_on_association(:posts).association_primary_key_type.type - assert_equal :string, Author.reflect_on_association(:essay).association_primary_key_type.type - - # Through Association - assert_equal :string, Author.reflect_on_association(:essay_category).association_primary_key_type.type - end - def test_association_primary_key_raises_when_missing_primary_key reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } @@ -352,36 +368,36 @@ class ReflectionTest < ActiveRecord::TestCase end def test_collection_association - assert Pirate.reflect_on_association(:birds).collection? - assert Pirate.reflect_on_association(:parrots).collection? + assert_predicate Pirate.reflect_on_association(:birds), :collection? + assert_predicate Pirate.reflect_on_association(:parrots), :collection? - assert !Pirate.reflect_on_association(:ship).collection? - assert !Ship.reflect_on_association(:pirate).collection? + assert_not_predicate Pirate.reflect_on_association(:ship), :collection? + assert_not_predicate Ship.reflect_on_association(:pirate), :collection? end def test_default_association_validation - assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm), :validate? - assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate? - assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate? + assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm), :validate? + assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm), :validate? end def test_always_validate_association_if_explicit - assert ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm).validate? - assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm).validate? - assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm).validate? + assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm), :validate? end def test_validate_association_if_autosave - assert ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm).validate? - assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm).validate? - assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm).validate? + assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm), :validate? + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm), :validate? end def test_never_validate_association_if_explicit - assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm).validate? - assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm).validate? - assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm).validate? + assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm), :validate? + assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm), :validate? + assert_not_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm), :validate? end def test_foreign_key diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index b68b3723f6..074ce9454f 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -58,7 +58,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_merging_with_locks devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked? + assert_predicate devs, :locked? end def test_relation_merging_with_preload @@ -72,6 +72,12 @@ class RelationMergingTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_left_outer_joins + comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.left_outer_joins(:author).where(body: "Such a lovely day")) + + assert_equal 1, comments.count + end + def test_relation_merging_with_association assert_queries(2) do # one for loading post, and another one merged query post = Post.where(body: "Such a lovely day").first @@ -107,9 +113,9 @@ class RelationMergingTest < ActiveRecord::TestCase def test_merging_with_from_clause relation = Post.all - assert relation.from_clause.empty? + assert_empty relation.from_clause relation = relation.merge(Post.from("posts")) - refute relation.from_clause.empty? + assert_not_empty relation.from_clause end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index ad3700b73a..1428b3e132 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -25,7 +25,7 @@ module ActiveRecord test "#order! with symbol prepends the table name" do assert relation.order!(:name).equal?(relation) node = relation.order_values.first - assert node.ascending? + assert_predicate node, :ascending? assert_equal :name, node.expr.name assert_equal "posts", node.expr.relation.name end @@ -88,7 +88,7 @@ module ActiveRecord assert relation.reorder!(:name).equal?(relation) node = relation.order_values.first - assert node.ascending? + assert_predicate node, :ascending? assert_equal :name, node.expr.name assert_equal "posts", node.expr.relation.name end @@ -139,7 +139,7 @@ module ActiveRecord private def relation - @relation ||= Relation.new(FakeKlass, Post.arel_table, Post.predicate_builder) + @relation ||= Relation.new(FakeKlass) end end end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index e5eb159d36..8703d238a0 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -75,8 +75,8 @@ class ActiveRecord::Relation end test "a clause knows if it is empty" do - assert WhereClause.empty.empty? - assert_not WhereClause.new(["anything"]).empty? + assert_empty WhereClause.empty + assert_not_empty WhereClause.new(["anything"]) end test "invert cannot handle nil" do diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index d95a54a2fe..99797528b2 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -265,7 +265,7 @@ module ActiveRecord end def test_where_with_decimal_for_string_column - count = Post.where(title: BigDecimal.new(0)).count + count = Post.where(title: BigDecimal(0)).count assert_equal 0, count end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 8362722e12..4e75371147 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -11,30 +11,29 @@ module ActiveRecord fixtures :posts, :comments, :authors, :author_addresses, :ratings def test_construction - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass, table: :b) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, "relation is not loaded" end def test_responds_to_model_and_returns_klass - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new(FakeKlass, :b, nil) - (Relation::SINGLE_VALUE_METHODS - [:create_with, :readonly]).each do |method| + relation = Relation.new(FakeKlass) + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - assert_equal false, relation.readonly_value value = relation.create_with_value assert_equal({}, value) assert_predicate value, :frozen? end def test_multi_value_initialize - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) Relation::MULTI_VALUE_METHODS.each do |method| values = relation.send("#{method}_values") assert_equal [], values, method.to_s @@ -43,49 +42,49 @@ module ActiveRecord end def test_extensions - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal({}, relation.where_values_hash) end def test_has_values - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation.where!(id: 10) assert_equal({ "id" => 10 }, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) - combine = left.and right + combine = left.or(right) relation.where! combine assert_equal({}, relation.where_values_hash) end def test_scope_for_create - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation.create_with_value = { hello: "world" } assert_equal({ "hello" => "world" }, relation.scope_for_create) end def test_create_with_value_with_wheres - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) assert_equal({}, relation.scope_for_create) relation.where!(id: 10) @@ -96,11 +95,11 @@ module ActiveRecord end def test_empty_scope - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - assert relation.empty_scope? + relation = Relation.new(Post) + assert_predicate relation, :empty_scope? relation.merge!(relation) - assert relation.empty_scope? + assert_predicate relation, :empty_scope? end def test_bad_constants_raise_errors @@ -110,31 +109,31 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new(FakeKlass, :b, nil) - assert !relation.eager_loading? + relation = Relation.new(FakeKlass) + assert_not_predicate relation, :eager_loading? end def test_eager_load_values - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) relation.eager_load! :b - assert relation.eager_loading? + assert_predicate relation, :eager_loading? end def test_references_values - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ["foo", "omg", "lol"], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) relation = relation.references(:foo).references(:foo) assert_equal ["foo"], relation.references_values end test "merging a hash into a relation" do - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = Relation.new(Post) relation = relation.merge where: { name: :lol }, readonly: true assert_equal({ "name" => :lol }, relation.where_clause.to_h) @@ -142,7 +141,7 @@ module ActiveRecord end test "merging an empty hash into a relation" do - assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause + assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass).merge({}).where_clause end test "merging a hash with unknown keys raises" do @@ -150,7 +149,7 @@ module ActiveRecord end test "merging nil or false raises" do - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) e = assert_raises(ArgumentError) do relation = relation.merge nil @@ -166,7 +165,7 @@ module ActiveRecord end test "#values returns a dup of the values" do - relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo) + relation = Relation.new(Post).where!(name: :foo) values = relation.values values[:where] = nil @@ -174,7 +173,7 @@ module ActiveRecord end test "relations can be created with a values hash" do - relation = Relation.new(FakeKlass, :b, nil, select: [:foo]) + relation = Relation.new(FakeKlass, values: { select: [:foo] }) assert_equal [:foo], relation.select_values end @@ -186,13 +185,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b, nil) + relation = Relation.new(klass) relation.merge!(where: ["foo = ?", "bar"]) assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause end def test_merging_readonly_false - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value @@ -236,17 +235,17 @@ module ActiveRecord def test_merge_raises_with_invalid_argument assert_raises ArgumentError do - relation = Relation.new(FakeKlass, :b, nil) + relation = Relation.new(FakeKlass) relation.merge(true) end end def test_respond_to_for_non_selected_element post = Post.select(:title).first - assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" + assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception" silence_warnings { post = Post.select("'title' as post_title").first } - assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception" + assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception" end def test_select_quotes_when_using_from_clause @@ -288,13 +287,18 @@ module ActiveRecord :string end + def cast(value) + raise value unless value == "value from user" + "cast value" + end + def deserialize(value) raise value unless value == "type cast for database" "type cast from database" end def serialize(value) - raise value unless value == "value from user" + raise value unless value == "cast value" "type cast for database" end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index eec43ef79e..bc7c54dbe0 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -56,7 +56,7 @@ class RelationTest < ActiveRecord::TestCase def test_dynamic_finder x = Post.where("author_id = ?", 1) - assert x.klass.respond_to?(:find_by_id), "@klass should handle dynamic finders" + assert_respond_to x.klass, :find_by_id end def test_multivalue_where @@ -98,7 +98,7 @@ class RelationTest < ActiveRecord::TestCase 2.times { assert_equal 5, topics.to_a.size } end - assert topics.loaded? + assert_predicate topics, :loaded? end def test_scoped_first @@ -108,7 +108,7 @@ class RelationTest < ActiveRecord::TestCase 2.times { assert_equal "The First Topic", topics.first.title } end - assert ! topics.loaded? + assert_not_predicate topics, :loaded? end def test_loaded_first @@ -119,7 +119,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal "The First Topic", topics.first.title end - assert topics.loaded? + assert_predicate topics, :loaded? end def test_loaded_first_with_limit @@ -131,7 +131,7 @@ class RelationTest < ActiveRecord::TestCase "The Second Topic of the day"], topics.first(2).map(&:title) end - assert topics.loaded? + assert_predicate topics, :loaded? end def test_first_get_more_than_available @@ -152,14 +152,14 @@ class RelationTest < ActiveRecord::TestCase 2.times { topics.to_a } end - assert topics.loaded? + assert_predicate topics, :loaded? original_size = topics.to_a.size Topic.create! title: "fake" assert_queries(1) { topics.reload } assert_equal original_size + 1, topics.size - assert topics.loaded? + assert_predicate topics, :loaded? end def test_finding_with_subquery @@ -250,7 +250,7 @@ class RelationTest < ActiveRecord::TestCase end def test_reverse_order_with_function - topics = Topic.order("length(title)").reverse_order + topics = Topic.order(Arel.sql("length(title)")).reverse_order assert_equal topics(:second).title, topics.first.title end @@ -260,24 +260,24 @@ class RelationTest < ActiveRecord::TestCase end def test_reverse_order_with_function_other_predicates - topics = Topic.order("author_name, length(title), id").reverse_order + topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order assert_equal topics(:second).title, topics.first.title - topics = Topic.order("length(author_name), id, length(title)").reverse_order + topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order assert_equal topics(:fifth).title, topics.first.title end def test_reverse_order_with_multiargument_function assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(author_name, title)").reverse_order + Topic.order(Arel.sql("concat(author_name, title)")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(lower(author_name), title)").reverse_order + Topic.order(Arel.sql("concat(lower(author_name), title)")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(author_name, lower(title))").reverse_order + Topic.order(Arel.sql("concat(author_name, lower(title))")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("concat(lower(author_name), title, length(title)").reverse_order + Topic.order(Arel.sql("concat(lower(author_name), title, length(title)")).reverse_order end end @@ -289,10 +289,10 @@ class RelationTest < ActiveRecord::TestCase def test_reverse_order_with_nulls_first_or_last assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("title NULLS FIRST").reverse_order + Topic.order(Arel.sql("title NULLS FIRST")).reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order("title nulls last").reverse_order + Topic.order(Arel.sql("title nulls last")).reverse_order end end @@ -385,29 +385,29 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_cross_table_order_and_limit tags = Tag.includes(:taggings). - order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)"). + order("tags.name asc", "taggings.taggable_id asc", Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")). limit(1).to_a assert_equal 1, tags.length end def test_finding_with_complex_order_and_limit - tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a + tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).limit(1).to_a assert_equal 1, tags.length end def test_finding_with_complex_order - tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a + tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).to_a assert_equal 3, tags.length end def test_finding_with_sanitized_order - query = Tag.order(["field(id, ?)", [1, 3, 2]]).to_sql + query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql assert_match(/field\(id, 1,3,2\)/, query) - query = Tag.order(["field(id, ?)", []]).to_sql + query = Tag.order([Arel.sql("field(id, ?)"), []]).to_sql assert_match(/field\(id, NULL\)/, query) - query = Tag.order(["field(id, ?)", nil]).to_sql + query = Tag.order([Arel.sql("field(id, ?)"), nil]).to_sql assert_match(/field\(id, NULL\)/, query) end @@ -501,12 +501,12 @@ class RelationTest < ActiveRecord::TestCase relation = Topic.all ["find_by_title", "find_by_title_and_author_name"].each do |method| - assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}" + assert_respond_to relation, method end end def test_respond_to_class_methods_and_scopes - assert Topic.all.respond_to?(:by_lifo) + assert_respond_to Topic.all, :by_lifo end def test_find_with_readonly_option @@ -601,7 +601,7 @@ class RelationTest < ActiveRecord::TestCase reader = Reader.create! post_id: post.id, person_id: 1 comment = Comment.create! post_id: post.id, body: "body" - assert !comment.respond_to?(:readers) + assert_not_respond_to comment, :readers post_rel = Post.preload(:readers).joins(:readers).where(title: "Uhuu") result_comment = Comment.joins(:post).merge(post_rel).to_a.first @@ -722,7 +722,7 @@ class RelationTest < ActiveRecord::TestCase def test_find_in_empty_array authors = Author.all.where(id: []) - assert authors.to_a.blank? + assert_predicate authors.to_a, :blank? end def test_where_with_ar_object @@ -780,8 +780,6 @@ class RelationTest < ActiveRecord::TestCase def test_find_all_using_where_with_relation david = authors(:david) - # switching the lines below would succeed in current rails - # assert_queries(2) { assert_queries(1) { relation = Author.where(id: Author.where(id: david.id)) assert_equal [david], relation.to_a @@ -820,8 +818,6 @@ class RelationTest < ActiveRecord::TestCase def test_find_all_using_where_with_relation_and_alternate_primary_key cool_first = minivans(:cool_first) - # switching the lines below would succeed in current rails - # assert_queries(2) { assert_queries(1) { relation = Minivan.where(minivan_id: Minivan.where(name: cool_first.name)) assert_equal [cool_first], relation.to_a @@ -867,19 +863,19 @@ class RelationTest < ActiveRecord::TestCase # Force load assert_equal [authors(:david)], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? assert_difference("Author.count", -1) { davids.destroy_all } assert_equal [], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? end def test_delete_all davids = Author.where(name: "David") assert_difference("Author.count", -1) { davids.delete_all } - assert ! davids.loaded? + assert_not_predicate davids, :loaded? end def test_delete_all_loaded @@ -887,20 +883,18 @@ class RelationTest < ActiveRecord::TestCase # Force load assert_equal [authors(:david)], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? assert_difference("Author.count", -1) { davids.delete_all } assert_equal [], davids.to_a - assert davids.loaded? + assert_predicate davids, :loaded? end def test_delete_all_with_unpermitted_relation_raises_error - assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } 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 } - assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } end def test_select_with_aggregates @@ -908,9 +902,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.count(:all) assert_equal 11, posts.size - assert posts.any? - assert posts.many? - assert_not posts.empty? + assert_predicate posts, :any? + assert_predicate posts, :many? + assert_not_empty posts end def test_select_takes_a_variable_list_of_args @@ -956,7 +950,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal author.posts.where(author_id: author.id).size, posts.count assert_equal 0, author.posts.where(author_id: another_author.id).size - assert author.posts.where(author_id: another_author.id).empty? + assert_empty author.posts.where(author_id: another_author.id) end def test_count_with_distinct @@ -969,6 +963,18 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.distinct(false).select(:comments_count).count end + def test_size_with_distinct + posts = Post.distinct.select(:author_id, :comments_count) + assert_queries(1) { assert_equal 8, posts.size } + assert_queries(1) { assert_equal 8, posts.load.size } + end + + def test_size_with_eager_loading_and_custom_order + posts = Post.includes(:comments).order("comments.id") + assert_queries(1) { assert_equal 11, posts.size } + 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" @@ -1000,7 +1006,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.all assert_queries(1) { assert_equal 11, posts.size } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? best_posts = posts.where(comments_count: 0) best_posts.load # force load @@ -1011,7 +1017,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.limit(10) assert_queries(1) { assert_equal 10, posts.size } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? best_posts = posts.where(comments_count: 0) best_posts.load # force load @@ -1022,7 +1028,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.limit(0) assert_no_queries { assert_equal 0, posts.size } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? posts.load # force load assert_no_queries { assert_equal 0, posts.size } @@ -1032,7 +1038,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.limit(0) assert_no_queries { assert_equal true, posts.empty? } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? end def test_count_complex_chained_relations @@ -1046,11 +1052,11 @@ class RelationTest < ActiveRecord::TestCase posts = Post.all assert_queries(1) { assert_equal false, posts.empty? } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? no_posts = posts.where(title: "") assert_queries(1) { assert_equal true, no_posts.empty? } - assert ! no_posts.loaded? + assert_not_predicate no_posts, :loaded? best_posts = posts.where(comments_count: 0) best_posts.load # force load @@ -1061,11 +1067,11 @@ class RelationTest < ActiveRecord::TestCase posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0") assert_queries(1) { assert_equal false, posts.empty? } - assert ! posts.loaded? + assert_not_predicate posts, :loaded? no_posts = posts.where(title: "") assert_queries(1) { assert_equal true, no_posts.empty? } - assert ! no_posts.loaded? + assert_not_predicate no_posts, :loaded? end def test_any @@ -1081,13 +1087,13 @@ class RelationTest < ActiveRecord::TestCase assert_queries(3) do assert posts.any? # Uses COUNT() - assert ! posts.where(id: nil).any? + assert_not_predicate posts.where(id: nil), :any? assert posts.any? { |p| p.id > 0 } assert ! posts.any? { |p| p.id <= 0 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_many @@ -1099,14 +1105,14 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.many? { |p| p.id < 2 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_many_with_limits posts = Post.all - assert posts.many? - assert ! posts.limit(1).many? + assert_predicate posts, :many? + assert_not_predicate posts.limit(1), :many? end def test_none? @@ -1115,14 +1121,14 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.none? # Uses COUNT() end - assert ! posts.loaded? + assert_not_predicate posts, :loaded? assert_queries(1) do assert posts.none? { |p| p.id < 0 } assert ! posts.none? { |p| p.id == 1 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_one @@ -1131,14 +1137,14 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.one? # Uses COUNT() end - assert ! posts.loaded? + assert_not_predicate posts, :loaded? assert_queries(1) do assert ! posts.one? { |p| p.id < 3 } assert posts.one? { |p| p.id == 1 } end - assert posts.loaded? + assert_predicate posts, :loaded? end def test_to_a_should_dup_target @@ -1171,10 +1177,10 @@ class RelationTest < ActiveRecord::TestCase sparrow = birds.create assert_kind_of Bird, sparrow - assert !sparrow.persisted? + assert_not_predicate sparrow, :persisted? hen = birds.where(name: "hen").create - assert hen.persisted? + assert_predicate hen, :persisted? assert_equal "hen", hen.name end @@ -1185,34 +1191,43 @@ class RelationTest < ActiveRecord::TestCase hen = birds.where(name: "hen").create! assert_kind_of Bird, hen - assert hen.persisted? + assert_predicate hen, :persisted? assert_equal "hen", hen.name end + def test_create_with_polymorphic_association + author = authors(:david) + post = posts(:welcome) + comment = Comment.where(post: post, author: author).create!(body: "hello") + + assert_equal author, comment.author + assert_equal post, comment.post + end + def test_first_or_create parrot = Bird.where(color: "green").first_or_create(name: "parrot") assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "parrot", parrot.name assert_equal "green", parrot.color same_parrot = Bird.where(color: "green").first_or_create(name: "parakeet") assert_kind_of Bird, same_parrot - assert same_parrot.persisted? + assert_predicate same_parrot, :persisted? assert_equal parrot, same_parrot end def test_first_or_create_with_no_parameters parrot = Bird.where(color: "green").first_or_create assert_kind_of Bird, parrot - assert !parrot.persisted? + assert_not_predicate parrot, :persisted? assert_equal "green", parrot.color end def test_first_or_create_with_block parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" } assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "green", parrot.color assert_equal "parrot", parrot.name @@ -1233,13 +1248,13 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_create_bang_with_valid_options parrot = Bird.where(color: "green").first_or_create!(name: "parrot") assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "parrot", parrot.name assert_equal "green", parrot.color same_parrot = Bird.where(color: "green").first_or_create!(name: "parakeet") assert_kind_of Bird, same_parrot - assert same_parrot.persisted? + assert_predicate same_parrot, :persisted? assert_equal parrot, same_parrot end @@ -1254,7 +1269,7 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_create_bang_with_valid_block parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" } assert_kind_of Bird, parrot - assert parrot.persisted? + assert_predicate parrot, :persisted? assert_equal "green", parrot.color assert_equal "parrot", parrot.name @@ -1285,9 +1300,9 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_initialize parrot = Bird.where(color: "green").first_or_initialize(name: "parrot") assert_kind_of Bird, parrot - assert !parrot.persisted? - assert parrot.valid? - assert parrot.new_record? + assert_not_predicate parrot, :persisted? + assert_predicate parrot, :valid? + assert_predicate parrot, :new_record? assert_equal "parrot", parrot.name assert_equal "green", parrot.color end @@ -1295,18 +1310,18 @@ class RelationTest < ActiveRecord::TestCase def test_first_or_initialize_with_no_parameters parrot = Bird.where(color: "green").first_or_initialize assert_kind_of Bird, parrot - assert !parrot.persisted? - assert !parrot.valid? - assert parrot.new_record? + assert_not_predicate parrot, :persisted? + assert_not_predicate parrot, :valid? + assert_predicate parrot, :new_record? assert_equal "green", parrot.color end def test_first_or_initialize_with_block parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" } assert_kind_of Bird, parrot - assert !parrot.persisted? - assert parrot.valid? - assert parrot.new_record? + assert_not_predicate parrot, :persisted? + assert_predicate parrot, :valid? + assert_predicate parrot, :new_record? assert_equal "green", parrot.color assert_equal "parrot", parrot.name end @@ -1315,7 +1330,7 @@ class RelationTest < ActiveRecord::TestCase assert_nil Bird.find_by(name: "bob") bird = Bird.find_or_create_by(name: "bob") - assert bird.persisted? + assert_predicate bird, :persisted? assert_equal bird, Bird.find_or_create_by(name: "bob") end @@ -1324,7 +1339,7 @@ class RelationTest < ActiveRecord::TestCase assert_nil Bird.find_by(name: "bob") bird = Bird.create_with(color: "green").find_or_create_by(name: "bob") - assert bird.persisted? + assert_predicate bird, :persisted? assert_equal "green", bird.color assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob") @@ -1338,7 +1353,7 @@ class RelationTest < ActiveRecord::TestCase assert_nil Bird.find_by(name: "bob") bird = Bird.find_or_initialize_by(name: "bob") - assert bird.new_record? + assert_predicate bird, :new_record? bird.save! assert_equal bird, Bird.find_or_initialize_by(name: "bob") @@ -1513,10 +1528,10 @@ class RelationTest < ActiveRecord::TestCase def test_doesnt_add_having_values_if_options_are_blank scope = Post.having("") - assert scope.having_clause.empty? + assert_empty scope.having_clause scope = Post.having([]) - assert scope.having_clause.empty? + assert_empty scope.having_clause end def test_having_with_binds_for_both_where_and_having @@ -1542,13 +1557,13 @@ class RelationTest < ActiveRecord::TestCase def test_references_triggers_eager_loading scope = Post.includes(:comments) - assert !scope.eager_loading? - assert scope.references(:comments).eager_loading? + assert_not_predicate scope, :eager_loading? + assert_predicate scope.references(:comments), :eager_loading? end def test_references_doesnt_trigger_eager_loading_if_reference_not_included scope = Post.references(:comments) - assert !scope.eager_loading? + assert_not_predicate scope, :eager_loading? end def test_automatically_added_where_references @@ -1579,7 +1594,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body") assert_equal ["comments"], scope.references_values - scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") + scope = Post.order(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")) if current_adapter?(:OracleAdapter) assert_equal ["COMMENTS"], scope.references_values else @@ -1596,7 +1611,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body asc") assert_equal ["comments"], scope.references_values - scope = Post.order("foo(comments.body)") + scope = Post.order(Arel.sql("foo(comments.body)")) assert_equal [], scope.references_values end @@ -1604,7 +1619,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body") assert_equal %w(comments), scope.references_values - scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") + scope = Post.reorder(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")) if current_adapter?(:OracleAdapter) assert_equal ["COMMENTS"], scope.references_values else @@ -1621,7 +1636,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body asc") assert_equal %w(comments), scope.references_values - scope = Post.reorder("foo(comments.body)") + scope = Post.reorder(Arel.sql("foo(comments.body)")) assert_equal [], scope.references_values end @@ -1656,7 +1671,7 @@ class RelationTest < ActiveRecord::TestCase # count always trigger the COUNT query. assert_queries(1) { topics.count } - assert topics.loaded? + assert_predicate topics, :loaded? end test "find_by with hash conditions returns the first matching record" do @@ -1831,7 +1846,7 @@ class RelationTest < ActiveRecord::TestCase test "delegations do not leak to other classes" do Topic.all.by_lifo assert Topic.all.class.method_defined?(:by_lifo) - assert !Post.all.respond_to?(:by_lifo) + assert_not_respond_to Post.all, :by_lifo end def test_unscope_with_subquery @@ -1856,7 +1871,7 @@ class RelationTest < ActiveRecord::TestCase def test_locked_should_not_build_arel posts = Post.locked - assert posts.locked? + assert_predicate posts, :locked? assert_nothing_raised { posts.lock!(false) } end @@ -1925,6 +1940,10 @@ class RelationTest < ActiveRecord::TestCase table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - ActiveRecord::Relation.create(Post, table_alias, predicate_builder) + ActiveRecord::Relation.create( + Post, + table: table_alias, + predicate_builder: predicate_builder + ) end end diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb index 0214dbec17..e32605fd11 100644 --- a/activerecord/test/cases/reserved_word_test.rb +++ b/activerecord/test/cases/reserved_word_test.rb @@ -39,7 +39,7 @@ class ReservedWordTest < ActiveRecord::TestCase t.string :order t.belongs_to :select end - @connection.create_table :values, force: true do |t| + @connection.create_table :values, primary_key: :as, force: true do |t| t.belongs_to :group end end @@ -88,6 +88,13 @@ class ReservedWordTest < ActiveRecord::TestCase assert_equal x, Group.find(x.id) end + def test_delete_all_with_subselect + create_test_fixtures :values + assert_equal 1, Values.order(:as).limit(1).offset(1).delete_all + assert_raise(ActiveRecord::RecordNotFound) { Values.find(2) } + assert Values.find(1) + end + def test_has_one_associations create_test_fixtures :group, :values v = Group.find(1).values @@ -109,7 +116,7 @@ class ReservedWordTest < ActiveRecord::TestCase end def test_activerecord_introspection - assert Group.table_exists? + assert_predicate Group, :table_exists? assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 85a555fe35..778cf86ac3 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/binary" require "models/author" require "models/post" +require "models/customer" class SanitizeTest < ActiveRecord::TestCase def setup @@ -11,30 +12,30 @@ class SanitizeTest < ActiveRecord::TestCase def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") - assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"]) - assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars]) + assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi"]) + assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") - assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"]) - assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper"]) + assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_bind_variables quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars]) + assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi"]) + assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_named_bind_variables quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"]) - assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1]) + assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"]) + assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) end def test_sanitize_sql_array_handles_relations @@ -43,42 +44,50 @@ class SanitizeTest < ActiveRecord::TestCase sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i - select_author_sql = Post.send(:sanitize_sql_array, ["id in (?)", david_posts]) + select_author_sql = Post.sanitize_sql_array(["id in (?)", david_posts]) assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for bind variables") - select_author_sql = Post.send(:sanitize_sql_array, ["id in (:post_ids)", post_ids: david_posts]) + select_author_sql = Post.sanitize_sql_array(["id in (:post_ids)", post_ids: david_posts]) assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for named bind variables") end def test_sanitize_sql_array_handles_empty_statement - select_author_sql = Post.send(:sanitize_sql_array, [""]) + select_author_sql = Post.sanitize_sql_array([""]) assert_equal("", select_author_sql) end def test_sanitize_sql_like - assert_equal '100\%', Binary.send(:sanitize_sql_like, "100%") - assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, "snake_cased_string") - assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint') - assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42") + assert_equal '100\%', Binary.sanitize_sql_like("100%") + assert_equal 'snake\_cased\_string', Binary.sanitize_sql_like("snake_cased_string") + assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint') + assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42") end def test_sanitize_sql_like_with_custom_escape_character - assert_equal "100!%", Binary.send(:sanitize_sql_like, "100%", "!") - assert_equal "snake!_cased!_string", Binary.send(:sanitize_sql_like, "snake_cased_string", "!") - assert_equal "great!!", Binary.send(:sanitize_sql_like, "great!", "!") - assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', "!") - assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42", "!") + assert_equal "100!%", Binary.sanitize_sql_like("100%", "!") + assert_equal "snake!_cased!_string", Binary.sanitize_sql_like("snake_cased_string", "!") + assert_equal "great!!", Binary.sanitize_sql_like("great!", "!") + assert_equal 'C:\\Programs\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint', "!") + assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42", "!") end def test_sanitize_sql_like_example_use_case searchable_post = Class.new(Post) do - def self.search(term) + def self.search_as_method(term) where("title LIKE ?", sanitize_sql_like(term, "!")) end + + scope :search_as_scope, -> (term) { + where("title LIKE ?", sanitize_sql_like(term, "!")) + } + end + + assert_sql(/LIKE '20!% !_reduction!_!!'/) do + searchable_post.search_as_method("20% _reduction_!").to_a end assert_sql(/LIKE '20!% !_reduction!_!!'/) do - searchable_post.search("20% _reduction_!").to_a + searchable_post.search_as_scope("20% _reduction_!").to_a end end @@ -159,6 +168,12 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call end + def test_deprecated_expand_hash_conditions_for_aggregates + assert_deprecated do + assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50))) + end + end + private def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index ac5092f1c1..a612ce9bb2 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -177,14 +177,14 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition - elsif current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) if ActiveRecord::Base.connection.supports_index_sort_order? assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition end + elsif ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end @@ -199,6 +199,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dumps_index_sort_order + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_rating/).first.strip + if ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating", order: :desc', index_definition + else + assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating"', index_definition + end + end + + def test_schema_dumps_index_length + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip + if current_adapter?(:Mysql2Adapter) + assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition + else + assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition + end + end + def test_schema_dump_should_honor_nonstandard_primary_keys output = standard_dump match = output.match(%r{create_table "movies"(.*)do}) diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 716ca29eda..0804de1fb3 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -120,49 +120,49 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_with_where_attributes expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(name: "David").unscope(where: :name).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort expected_2 = Developer.order("salary DESC").collect(&:name) received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({ where: :name }, :select).collect(&:name) - assert_equal expected_2, received_2 + assert_equal expected_2.sort, received_2.sort expected_3 = Developer.order("salary DESC").collect(&:name) received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name) - assert_equal expected_3, received_3 + assert_equal expected_3.sort, received_3.sort expected_4 = Developer.order("salary DESC").collect(&:name) received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name) - assert_equal expected_4, received_4 + assert_equal expected_4.sort, received_4.sort expected_5 = Developer.order("salary DESC").collect(&:name) received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) - assert_equal expected_5, received_5 + assert_equal expected_5.sort, received_5.sort expected_6 = Developer.order("salary DESC").collect(&:name) received_6 = DeveloperOrderedBySalary.where(Developer.arel_table["name"].eq("David")).unscope(where: :name).collect(&:name) - assert_equal expected_6, received_6 + assert_equal expected_6.sort, received_6.sort expected_7 = Developer.order("salary DESC").collect(&:name) received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq("David")).unscope(where: :name).collect(&:name) - assert_equal expected_7, received_7 + assert_equal expected_7.sort, received_7.sort end def test_unscope_comparison_where_clauses # unscoped for WHERE (`developers`.`id` <= 2) expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name } - assert_equal expected, received + assert_equal expected.sort, received.sort # unscoped for WHERE (`developers`.`id` < 2) expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name } - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_unscope_multiple_where_clauses expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.where(name: "Jamis").where(id: 1).unscope(where: [:name, :id]).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_unscope_string_where_clauses_involved @@ -172,23 +172,23 @@ class DefaultScopingTest < ActiveRecord::TestCase dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("created_at > ?", 1.year.ago) received = dev_ordered_relation.unscope(where: [:name]).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_unscope_with_grouping_attributes expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort expected_2 = Developer.order("salary DESC").collect(&:name) received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name) - assert_equal expected_2, received_2 + assert_equal expected_2.sort, received_2.sort end def test_unscope_with_limit_in_query expected = Developer.order("salary DESC").collect(&:name) received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name) - assert_equal expected, received + assert_equal expected.sort, received.sort end def test_order_to_unscope_reordering @@ -224,6 +224,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_left_outer_joins + expected = Developer.all.collect(&:name) + received = Developer.left_outer_joins(:projects).select(:id).unscope(:left_outer_joins, :select).collect(&:name) + assert_equal expected, received + end + + def test_unscope_left_joins + expected = Developer.all.collect(&:name) + received = Developer.left_joins(:projects).select(:id).unscope(:left_joins, :select).collect(&:name) + assert_equal expected, received + end + def test_unscope_includes expected = Developer.all.collect(&:name) received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name) @@ -290,8 +302,8 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_merging merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where)) - assert merged.where_clause.empty? - assert !merged.where(name: "Jon").where_clause.empty? + assert_empty merged.where_clause + assert_not_empty merged.where(name: "Jon").where_clause end def test_order_in_default_scope_should_not_prevail @@ -472,7 +484,7 @@ class DefaultScopingTest < ActiveRecord::TestCase test "a scope can remove the condition from the default scope" do scope = DeveloperCalledJamis.david2 assert_equal 1, scope.where_clause.ast.children.length - assert_equal Developer.where(name: "David"), scope + assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id) end def test_with_abstract_class_where_clause_should_not_be_duplicated diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index b0431a4e34..ea71a5ce28 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -13,7 +13,7 @@ class NamedScopingTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable - assert !Topic.all.empty? + assert_not_empty Topic.all assert_equal Topic.all.to_a, Topic.base assert_equal Topic.all.to_a, Topic.base.to_a @@ -40,7 +40,7 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_delegates_finds_and_calculations_to_the_base_class - assert !Topic.all.empty? + assert_not_empty Topic.all assert_equal Topic.all.to_a, Topic.base.to_a assert_equal Topic.first, Topic.base.first @@ -65,13 +65,13 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy - assert Topic.approved.respond_to?(:limit) - assert Topic.approved.respond_to?(:count) - assert Topic.approved.respond_to?(:length) + assert_respond_to Topic.approved, :limit + assert_respond_to Topic.approved, :count + assert_respond_to Topic.approved, :length end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.all.merge!(where: { approved: true }).to_a.empty? + assert_not_empty Topic.all.merge!(where: { approved: true }).to_a assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved assert_equal Topic.where(approved: true).count, Topic.approved.count @@ -87,7 +87,7 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved) assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied) assert !(approved == replied) - assert !(approved & replied).empty? + assert_not_empty (approved & replied) assert_equal approved & replied, Topic.approved.replied end @@ -115,7 +115,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_has_many_associations_have_access_to_scopes assert_not_equal Post.containing_the_letter_a, authors(:david).posts - assert !Post.containing_the_letter_a.empty? + assert_not_empty Post.containing_the_letter_a expected = authors(:david).posts & Post.containing_the_letter_a assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id) @@ -128,15 +128,15 @@ class NamedScopingTest < ActiveRecord::TestCase def test_has_many_through_associations_have_access_to_scopes assert_not_equal Comment.containing_the_letter_e, authors(:david).comments - assert !Comment.containing_the_letter_e.empty? + assert_not_empty Comment.containing_the_letter_e expected = authors(:david).comments & Comment.containing_the_letter_e assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id) end def test_scopes_honor_current_scopes_from_when_defined - assert !Post.ranked_by_comments.limit_by(5).empty? - assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty? + assert_not_empty Post.ranked_by_comments.limit_by(5) + assert_not_empty authors(:david).posts.ranked_by_comments.limit_by(5) assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5) assert_not_equal Post.top(5), authors(:david).posts.top(5) # Oracle sometimes sorts differently if WHERE condition is changed @@ -151,15 +151,31 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "The scope body needs to be callable.", e.message end + def test_scopes_name_is_relation_method + conflicts = [ + :records, + :to_ary, + :to_sql, + :explain + ] + + conflicts.each do |name| + e = assert_raises ArgumentError do + Class.new(Post).class_eval { scope name, -> { where(approved: true) } } + end + assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) + end + end + def test_active_records_have_scope_named__all__ - assert !Topic.all.empty? + assert_not_empty Topic.all assert_equal Topic.all.to_a, Topic.base end def test_active_records_have_scope_named__scoped__ scope = Topic.where("content LIKE '%Have%'") - assert !scope.empty? + assert_not_empty scope assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'") end @@ -212,9 +228,9 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_model_class_should_respond_to_any - assert Topic.any? + assert_predicate Topic, :any? Topic.delete_all - assert !Topic.any? + assert_not_predicate Topic, :any? end def test_many_should_not_load_results @@ -243,22 +259,22 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_return_false_if_none_or_one topics = Topic.base.where(id: 0) - assert !topics.many? + assert_not_predicate topics, :many? topics = Topic.base.where(id: 1) - assert !topics.many? + assert_not_predicate topics, :many? end def test_many_should_return_true_if_more_than_one - assert Topic.base.many? + assert_predicate Topic.base, :many? end def test_model_class_should_respond_to_many Topic.delete_all - assert !Topic.many? + assert_not_predicate Topic, :many? Topic.create! - assert !Topic.many? + assert_not_predicate Topic, :many? Topic.create! - assert Topic.many? + assert_predicate Topic, :many? end def test_should_build_on_top_of_scope @@ -407,16 +423,16 @@ class NamedScopingTest < ActiveRecord::TestCase def test_chaining_applies_last_conditions_when_creating post = Topic.rejected.new - assert !post.approved? + assert_not_predicate post, :approved? post = Topic.rejected.approved.new - assert post.approved? + assert_predicate post, :approved? post = Topic.approved.rejected.new - assert !post.approved? + assert_not_predicate post, :approved? post = Topic.approved.rejected.approved.new - assert post.approved? + assert_predicate post, :approved? end def test_chaining_combines_conditions_when_searching @@ -482,7 +498,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_index_on_scope approved = Topic.approved.order("id ASC") assert_equal topics(:second), approved[0] - assert approved.loaded? + assert_predicate approved, :loaded? end def test_nested_scopes_queries_size @@ -562,16 +578,16 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_model_class_should_respond_to_none - assert !Topic.none? + assert_not_predicate Topic, :none? Topic.delete_all - assert Topic.none? + assert_predicate Topic, :none? end def test_model_class_should_respond_to_one - assert !Topic.one? + assert_not_predicate Topic, :one? Topic.delete_all - assert !Topic.one? + assert_not_predicate Topic, :one? Topic.create! - assert Topic.one? + assert_predicate Topic, :one? end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 116f8e83aa..5c86bc892d 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -213,21 +213,21 @@ class RelationScopingTest < ActiveRecord::TestCase def test_current_scope_does_not_pollute_sibling_subclasses Comment.none.scoping do - assert_not SpecialComment.all.any? - assert_not VerySpecialComment.all.any? - assert_not SubSpecialComment.all.any? + assert_not_predicate SpecialComment.all, :any? + assert_not_predicate VerySpecialComment.all, :any? + assert_not_predicate SubSpecialComment.all, :any? end SpecialComment.none.scoping do - assert Comment.all.any? - assert VerySpecialComment.all.any? - assert_not SubSpecialComment.all.any? + assert_predicate Comment.all, :any? + assert_predicate VerySpecialComment.all, :any? + assert_not_predicate SubSpecialComment.all, :any? end SubSpecialComment.none.scoping do - assert Comment.all.any? - assert VerySpecialComment.all.any? - assert SpecialComment.all.any? + assert_predicate Comment.all, :any? + assert_predicate VerySpecialComment.all, :any? + assert_predicate SpecialComment.all, :any? end end @@ -334,7 +334,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase def test_nested_exclusive_scope_for_create comment = Comment.create_with(body: "Hey guys, nested scopes are broken. Please fix!").scoping do Comment.unscoped.create_with(post_id: 1).scoping do - assert Comment.new.body.blank? + assert_predicate Comment.new.body, :blank? Comment.create body: "Hey guys" end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 32dafbd458..7de5429cbb 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -279,7 +279,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.new(content: nil) - assert_not topic.content_changed? + assert_not_predicate topic, :content_changed? end def test_classes_without_no_arg_constructors_are_not_supported @@ -349,7 +349,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = model.create!(foo: "bar") topic.foo - refute topic.changed? + assert_not_predicate topic, :changed? end def test_serialized_attribute_works_under_concurrent_initial_access diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index 1f715e41a6..ad6cd198e2 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -12,7 +12,6 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end - #Cache v 1.1 tests def test_statement_cache Book.create(name: "my book") Book.create(name: "my other book") @@ -51,8 +50,6 @@ module ActiveRecord assert_equal("my other book", b.name) end - #End - def test_statement_cache_with_simple_statement cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book").where("author_id > 3") diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index ebf4016960..a30d13632a 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -45,7 +45,7 @@ class StoreTest < ActiveRecord::TestCase test "updating the store will mark it as changed" do @john.color = "red" - assert @john.settings_changed? + assert_predicate @john, :settings_changed? end test "updating the store populates the changed array correctly" do @@ -56,7 +56,7 @@ class StoreTest < ActiveRecord::TestCase test "updating the store won't mark it as changed if an attribute isn't changed" do @john.color = @john.color - assert !@john.settings_changed? + assert_not_predicate @john, :settings_changed? end test "object initialization with not nullable column" do @@ -137,7 +137,7 @@ class StoreTest < ActiveRecord::TestCase test "updating the store will mark it as changed encoded with JSON" do @john.height = "short" - assert @john.json_data_changed? + assert_predicate @john, :json_data_changed? end test "object initialization with not nullable column encoded with JSON" do diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 5a094ead42..21226352ff 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -28,15 +28,32 @@ module ActiveRecord class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments.dup - current_env = ActiveRecord::Migrator.current_environment + 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 + ActiveRecord::Base.protected_environments = [current_env] + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + 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 ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end @@ -46,7 +63,7 @@ module ActiveRecord def test_raises_an_error_if_no_migrations_have_been_made ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! @@ -93,6 +110,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path) assert File.file?(path) ensure + ActiveRecord::Base.clear_cache! FileUtils.rm_rf(path) end end @@ -329,50 +347,92 @@ module ActiveRecord end end - class DatabaseTasksMigrateTest < ActiveRecord::TestCase - self.use_transactional_tests = false + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class DatabaseTasksMigrateTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + # Use a memory db here to avoid having to rollback at the end + setup do + migrations_path = MIGRATIONS_ROOT + "/valid" + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", + database: ":memory:", migrations_paths: migrations_path + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end - def setup - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = "custom/path" - end + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end - def teardown - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil - end + def test_migrate_set_and_unset_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" - def test_migrate_receives_correct_env_vars - verbose, version = ENV["VERBOSE"], ENV["VERSION"] + # run down migration because it was already run on copied db + assert_empty capture_migration_output - ENV["VERBOSE"] = "false" - ENV["VERSION"] = "4" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4) - ActiveRecord::Migration.expects(:verbose=).with(false) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + ENV.delete("VERSION") + ENV.delete("VERBOSE") - ENV.delete("VERBOSE") - ENV.delete("VERSION") - ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + # re-run up migration + assert_includes capture_migration_output, "migrating" + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end - ENV["VERBOSE"] = "" - ENV["VERSION"] = "" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + def test_migrate_set_and_unset_empty_values_for_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] - ENV["VERBOSE"] = "yes" - ENV["VERSION"] = "0" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate - ensure - ENV["VERBOSE"], ENV["VERSION"] = verbose, version + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + # run down migration because it was already run on copied db + assert_empty capture_migration_output + + ENV["VERBOSE"] = "" + ENV["VERSION"] = "" + + # re-run up migration + assert_includes capture_migration_output, "migrating" + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + def test_migrate_set_and_unset_nonsense_values_for_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + + # run down migration because it was already run on copied db + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + assert_empty capture_migration_output + + ENV["VERBOSE"] = "yes" + ENV["VERSION"] = "2" + + # run no migration because 2 was already run + assert_empty capture_migration_output + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + private + def capture_migration_output + capture(:stdout) do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end end + end + + class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase + self.use_transactional_tests = false def test_migrate_raise_error_on_invalid_version_format version = ENV["VERSION"] diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 06a8693a7d..024b5bd8a1 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -77,6 +77,10 @@ module ActiveRecord model.reset_column_information model.column_names.include?(column_name.to_s) end + + def frozen_error_class + Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError + end end class PostgreSQLTestCase < TestCase @@ -112,7 +116,7 @@ module ActiveRecord # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im] mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 5d3e2a175c..e95446c0a7 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -90,7 +90,7 @@ class TimestampTest < ActiveRecord::TestCase @developer.touch(:created_at) end - assert !@developer.created_at_changed? , "created_at should not be changed" + assert !@developer.created_at_changed?, "created_at should not be changed" assert !@developer.changed?, "record should not be changed" assert_not_equal previously_created_at, @developer.created_at assert_not_equal @previously_updated_at, @developer.updated_at @@ -139,13 +139,13 @@ class TimestampTest < ActiveRecord::TestCase def test_touching_a_no_touching_object Developer.no_touching do - assert @developer.no_touching? - assert !@owner.no_touching? + assert_predicate @developer, :no_touching? + assert_not_predicate @owner, :no_touching? @developer.touch end - assert !@developer.no_touching? - assert !@owner.no_touching? + assert_not_predicate @developer, :no_touching? + assert_not_predicate @owner, :no_touching? assert_equal @previously_updated_at, @developer.updated_at end @@ -162,26 +162,26 @@ class TimestampTest < ActiveRecord::TestCase def test_global_no_touching ActiveRecord::Base.no_touching do - assert @developer.no_touching? - assert @owner.no_touching? + assert_predicate @developer, :no_touching? + assert_predicate @owner, :no_touching? @developer.touch end - assert !@developer.no_touching? - assert !@owner.no_touching? + assert_not_predicate @developer, :no_touching? + assert_not_predicate @owner, :no_touching? assert_equal @previously_updated_at, @developer.updated_at end def test_no_touching_threadsafe Thread.new do Developer.no_touching do - assert @developer.no_touching? + assert_predicate @developer, :no_touching? sleep(1) end end - assert !@developer.no_touching? + assert_not_predicate @developer, :no_touching? end def test_no_touching_with_callbacks @@ -237,7 +237,7 @@ class TimestampTest < ActiveRecord::TestCase pet = Pet.new(owner: klass.new) pet.save! - assert pet.owner.new_record? + assert_predicate pet.owner, :new_record? end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 1757031371..925a4609a2 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -13,7 +13,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_laster_raise_if_non_persisted invoice = Invoice.new Invoice.transaction do - assert_not invoice.persisted? + assert_not_predicate invoice, :persisted? assert_raises(ActiveRecord::ActiveRecordError) do invoice.touch_later end @@ -23,7 +23,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_set_dirty_attributes invoice = Invoice.create! invoice.touch_later - assert_not invoice.changed? + assert_not_predicate invoice, :changed? end def test_touch_later_respects_no_touching_policy diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 1f370a80ee..1c7cec4dad 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -158,13 +158,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def test_only_call_after_commit_on_top_level_transactions @first.after_commit_block { |r| r.history << :after_commit } - assert @first.history.empty? + assert_empty @first.history @first.transaction do @first.transaction(requires_new: true) do @first.touch end - assert @first.history.empty? + assert_empty @first.history end assert_equal [:after_commit], @first.history end @@ -518,7 +518,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.content = "foo" @topic.save! end - assert @topic.history.empty? + assert_empty @topic.history end def test_commit_run_transactions_callbacks_with_explicit_enrollment @@ -538,7 +538,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.save! raise ActiveRecord::Rollback end - assert @topic.history.empty? + assert_empty @topic.history end def test_rollback_run_transactions_callbacks_with_explicit_enrollment diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 5c8ae4d3cb..c70286d52a 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -20,22 +20,22 @@ class TransactionTest < ActiveRecord::TestCase def test_persisted_in_a_model_with_custom_primary_key_after_failed_save movie = Movie.create - assert !movie.persisted? + assert_not_predicate movie, :persisted? end def test_raise_after_destroy - assert_not @first.frozen? + assert_not_predicate @first, :frozen? assert_raises(RuntimeError) { Topic.transaction do @first.destroy - assert @first.frozen? + assert_predicate @first, :frozen? raise end } assert @first.reload - assert_not @first.frozen? + assert_not_predicate @first, :frozen? end def test_successful @@ -152,7 +152,7 @@ class TransactionTest < ActiveRecord::TestCase @first.approved = true e = assert_raises(RuntimeError) { @first.save } assert_equal "Make the transaction rollback", e.message - assert !Topic.find(1).approved? + assert_not_predicate Topic.find(1), :approved? end def test_rolling_back_in_a_callback_rollbacks_before_save @@ -186,7 +186,7 @@ class TransactionTest < ActiveRecord::TestCase author = Author.create! name: "foo" author.name = nil assert_not author.save - assert_not author.new_record? + assert_not_predicate author, :new_record? end def test_update_should_rollback_on_failure @@ -417,8 +417,8 @@ class TransactionTest < ActiveRecord::TestCase end end - assert @first.reload.approved? - assert !@second.reload.approved? + assert_predicate @first.reload, :approved? + assert_not_predicate @second.reload, :approved? end if Topic.connection.supports_savepoints? def test_force_savepoint_on_instance @@ -438,8 +438,8 @@ class TransactionTest < ActiveRecord::TestCase end end - assert @first.reload.approved? - assert !@second.reload.approved? + assert_predicate @first.reload, :approved? + assert_not_predicate @second.reload, :approved? end if Topic.connection.supports_savepoints? def test_no_savepoint_in_nested_transaction_without_force @@ -459,8 +459,8 @@ class TransactionTest < ActiveRecord::TestCase end end - assert !@first.reload.approved? - assert !@second.reload.approved? + assert_not_predicate @first.reload, :approved? + assert_not_predicate @second.reload, :approved? end if Topic.connection.supports_savepoints? def test_many_savepoints @@ -516,12 +516,12 @@ class TransactionTest < ActiveRecord::TestCase @first.approved = false @first.save! Topic.connection.rollback_to_savepoint("first") - assert @first.reload.approved? + assert_predicate @first.reload, :approved? @first.approved = false @first.save! Topic.connection.release_savepoint("first") - assert_not @first.reload.approved? + assert_not_predicate @first.reload, :approved? end end if Topic.connection.supports_savepoints? @@ -576,7 +576,7 @@ class TransactionTest < ActiveRecord::TestCase def test_rollback_when_saving_a_frozen_record topic = Topic.new(title: "test") topic.freeze - e = assert_raise(RuntimeError) { topic.save } + e = assert_raise(frozen_error_class) { topic.save } # Not good enough, but we can't do much # about it since there is no specific error # for frozen objects. @@ -663,8 +663,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - assert_not reply.frozen? - assert_not topic.frozen? + assert_not_predicate reply, :frozen? + assert_not_predicate topic, :frozen? end def test_restore_id_after_rollback @@ -819,28 +819,28 @@ class TransactionTest < ActiveRecord::TestCase connection = Topic.connection transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction - assert transaction.open? - assert !transaction.state.rolledback? - assert !transaction.state.committed? + assert_predicate transaction, :open? + assert_not_predicate transaction.state, :rolledback? + assert_not_predicate transaction.state, :committed? transaction.rollback - assert transaction.state.rolledback? - assert !transaction.state.committed? + assert_predicate transaction.state, :rolledback? + assert_not_predicate transaction.state, :committed? end def test_transactions_state_from_commit connection = Topic.connection transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction - assert transaction.open? - assert !transaction.state.rolledback? - assert !transaction.state.committed? + assert_predicate transaction, :open? + assert_not_predicate transaction.state, :rolledback? + assert_not_predicate transaction.state, :committed? transaction.commit - assert !transaction.state.rolledback? - assert transaction.state.committed? + assert_not_predicate transaction.state, :rolledback? + assert_predicate transaction.state, :committed? end def test_set_state_method_is_deprecated @@ -929,7 +929,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase raise end rescue - assert !@first.reload.approved? + assert_not_predicate @first.reload, :approved? end end @@ -950,7 +950,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase end end - assert !@first.reload.approved? + assert_not_predicate @first.reload, :approved? end end if Topic.connection.supports_savepoints? diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 8c51b30fdd..9e7810a6a5 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -9,16 +9,16 @@ module ActiveRecord klass.table_name = "authors" author = klass.create!(name: "Sean") - assert_not author.changed? + assert_not_predicate author, :changed? author.name << " Griffin" - assert author.name_changed? + assert_predicate author, :name_changed? author.save! author.reload assert_equal "Sean Griffin", author.name - assert_not author.changed? + assert_not_predicate author, :changed? end end end diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb new file mode 100644 index 0000000000..72d4997d0b --- /dev/null +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" +require "models/comment" + +class UnsafeRawSqlTest < ActiveRecord::TestCase + fixtures :posts, :comments + + test "order: allows string column name" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows symbol column name" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:title).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:title).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows downcase symbol direction" do + ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :asc).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :asc).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows upcase symbol direction" do + ids_expected = Post.order(Arel.sql("title") => Arel.sql("ASC")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :ASC).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :ASC).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows string direction" do + ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: "asc").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: "asc").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows multiple columns" do + ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, :title).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, :title).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows mixed" do + ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title") => Arel.sql("asc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, title: :asc).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, title: :asc).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows table and column name" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows column name and direction in string" do + ids_expected = Post.order(Arel.sql("title desc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title desc").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title desc").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: allows table name, column name and direction in string" do + ids_expected = Post.order(Arel.sql("title desc")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title desc").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title desc").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: disallows invalid column name" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order("len(title) asc").pluck(:id) + end + end + end + + test "order: disallows invalid direction" do + with_unsafe_raw_sql_disabled do + assert_raises(ArgumentError) do + Post.order(title: :foo).pluck(:id) + end + end + end + + test "order: disallows invalid column with direction" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order("len(title)" => :asc).pluck(:id) + end + end + end + + test "order: always allows Arel" do + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("length(title)")).pluck(:title) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("length(title)")).pluck(:title) } + + assert_equal ids_depr, ids_disabled + end + + test "order: allows Arel.sql with binds" do + ids_expected = Post.order(Arel.sql("REPLACE(title, 'misc', 'zzzz'), id")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: disallows invalid bind statement" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order(["REPLACE(title, ?, ?), id", "misc", "zzzz"]).pluck(:id) + end + end + end + + test "order: disallows invalid Array arguments" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.order(["author_id", "length(title)"]).pluck(:id) + end + end + end + + test "order: allows valid Array arguments" do + ids_expected = Post.order(Arel.sql("author_id, length(title)")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + + test "order: logs deprecation warning for unrecognized column" do + with_unsafe_raw_sql_deprecated do + assert_deprecated(/Dangerous query method/) do + Post.order("length(title)") + end + end + end + + test "pluck: allows string column name" do + titles_expected = Post.pluck(Arel.sql("title")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("title") } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("title") } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + + test "pluck: allows symbol column name" do + titles_expected = Post.pluck(Arel.sql("title")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title) } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title) } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + + test "pluck: allows multiple column names" do + values_expected = Post.pluck(Arel.sql("title"), Arel.sql("id")) + + values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title, :id) } + values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title, :id) } + + assert_equal values_expected, values_depr + assert_equal values_expected, values_disabled + end + + test "pluck: allows column names with includes" do + values_expected = Post.includes(:comments).pluck(Arel.sql("title"), Arel.sql("id")) + + values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, :id) } + values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, :id) } + + assert_equal values_expected, values_depr + assert_equal values_expected, values_disabled + end + + test "pluck: allows auto-generated attributes" do + values_expected = Post.pluck(Arel.sql("tags_count")) + + values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:tags_count) } + values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:tags_count) } + + assert_equal values_expected, values_depr + assert_equal values_expected, values_disabled + end + + test "pluck: allows table and column names" do + titles_expected = Post.pluck(Arel.sql("title")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("posts.title") } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("posts.title") } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + + test "pluck: disallows invalid column name" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.pluck("length(title)") + end + end + end + + test "pluck: disallows invalid column name amongst valid names" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.pluck(:title, "length(title)") + end + end + end + + test "pluck: disallows invalid column names with includes" do + with_unsafe_raw_sql_disabled do + assert_raises(ActiveRecord::UnknownAttributeReference) do + Post.includes(:comments).pluck(:title, "length(title)") + end + end + end + + test "pluck: always allows Arel" do + values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) } + values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) } + + assert_equal values_depr, values_disabled + end + + test "pluck: logs deprecation warning" do + with_unsafe_raw_sql_deprecated do + assert_deprecated(/Dangerous query method/) do + Post.includes(:comments).pluck(:title, "length(title)") + end + end + end + + def with_unsafe_raw_sql_disabled(&blk) + with_config(:disabled, &blk) + end + + def with_unsafe_raw_sql_deprecated(&blk) + with_config(:deprecated, &blk) + end + + def with_config(new_value, &blk) + old_value = ActiveRecord::Base.allow_unsafe_raw_sql + ActiveRecord::Base.allow_unsafe_raw_sql = new_value + blk.call + ensure + ActiveRecord::Base.allow_unsafe_raw_sql = old_value + end +end diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index a997f8be9c..8235a54d8a 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -13,8 +13,8 @@ class AbsenceValidationTest < ActiveRecord::TestCase validates_absence_of :name end - assert boy_klass.new.valid? - assert_not boy_klass.new(name: "Alex").valid? + assert_predicate boy_klass.new, :valid? + assert_not_predicate boy_klass.new(name: "Alex"), :valid? end def test_has_one_marked_for_destruction @@ -44,7 +44,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase assert_not boy.valid?, "should not be valid if has_many association is present" i2.mark_for_destruction - assert boy.valid? + assert_predicate boy, :valid? end def test_does_not_call_to_a_on_associations @@ -65,11 +65,11 @@ class AbsenceValidationTest < ActiveRecord::TestCase Interest.validates_absence_of(:token) interest = Interest.create!(topic: "Thought Leadering") - assert interest.valid? + assert_predicate interest, :valid? interest.token = "tl" - assert interest.invalid? + assert_predicate interest, :invalid? end end end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 80fe375ae5..ce6d42b34b 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -16,14 +16,14 @@ class AssociationValidationTest < ActiveRecord::TestCase Reply.validates_presence_of(:content) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")] - assert !t.valid? - assert t.errors[:replies].any? + assert_not_predicate t, :valid? + assert_predicate t.errors[:replies], :any? assert_equal 1, r.errors.count # make sure all associated objects have been validated assert_equal 0, r2.errors.count assert_equal 1, r3.errors.count assert_equal 0, r4.errors.count r.content = r3.content = "non-empty" - assert t.valid? + assert_predicate t, :valid? end def test_validates_associated_one @@ -31,10 +31,10 @@ class AssociationValidationTest < ActiveRecord::TestCase Topic.validates_presence_of(:content) r = Reply.new("title" => "A reply", "content" => "with content!") r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? - assert r.errors[:topic].any? + assert_not_predicate r, :valid? + assert_predicate r.errors[:topic], :any? r.topic.content = "non-empty" - assert r.valid? + assert_predicate r, :valid? end def test_validates_associated_marked_for_destruction @@ -42,9 +42,9 @@ class AssociationValidationTest < ActiveRecord::TestCase Reply.validates_presence_of(:content) t = Topic.new t.replies << Reply.new - assert t.invalid? + assert_predicate t, :invalid? t.replies.first.mark_for_destruction - assert t.valid? + assert_predicate t, :valid? end def test_validates_associated_without_marked_for_destruction @@ -56,7 +56,7 @@ class AssociationValidationTest < ActiveRecord::TestCase Topic.validates_associated(:replies) t = Topic.new t.define_singleton_method(:replies) { [reply.new] } - assert t.valid? + assert_predicate t, :valid? end def test_validates_associated_with_custom_message_using_quotes @@ -71,11 +71,11 @@ class AssociationValidationTest < ActiveRecord::TestCase def test_validates_associated_missing Reply.validates_presence_of(:topic) r = Reply.create("title" => "A reply", "content" => "with content!") - assert !r.valid? - assert r.errors[:topic].any? + assert_not_predicate r, :valid? + assert_predicate r.errors[:topic], :any? r.topic = Topic.first - assert r.valid? + assert_predicate r, :valid? end def test_validates_presence_of_belongs_to_association__parent_is_new_record diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 87ce4c6f37..73422a31cd 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -18,47 +18,47 @@ class LengthValidationTest < ActiveRecord::TestCase assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 } o = @owner.new("name" => "nopets") assert !o.save - assert o.errors[:pets].any? + assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") - assert o.valid? + assert_predicate o, :valid? end def test_validates_size_of_association_using_within assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 } o = @owner.new("name" => "nopets") assert !o.save - assert o.errors[:pets].any? + assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") - assert o.valid? + assert_predicate o, :valid? 2.times { o.pets.build("name" => "apet") } assert !o.save - assert o.errors[:pets].any? + assert_predicate o.errors[:pets], :any? end def test_validates_size_of_association_utf8 @owner.validates_size_of :pets, minimum: 1 o = @owner.new("name" => "あいうえおかきくけこ") assert !o.save - assert o.errors[:pets].any? + assert_predicate o.errors[:pets], :any? o.pets.build("name" => "あいうえおかきくけこ") - assert o.valid? + assert_predicate o, :valid? end def test_validates_size_of_respects_records_marked_for_destruction @owner.validates_size_of :pets, minimum: 1 owner = @owner.new assert_not owner.save - assert owner.errors[:pets].any? + assert_predicate owner.errors[:pets], :any? pet = owner.pets.build - assert owner.valid? + assert_predicate owner, :valid? assert owner.save pet_count = Pet.count assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ] - assert_not owner.valid? - assert owner.errors[:pets].any? + assert_not_predicate owner, :valid? + assert_predicate owner.errors[:pets], :any? assert_equal pet_count, Pet.count end @@ -70,11 +70,11 @@ class LengthValidationTest < ActiveRecord::TestCase pet = Pet.create!(name: "Fancy Pants", nickname: "Fancy") - assert pet.valid? + assert_predicate pet, :valid? pet.nickname = "" - assert pet.invalid? + assert_predicate pet, :invalid? end end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 3ab1567b51..63c3f67da2 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -15,10 +15,10 @@ class PresenceValidationTest < ActiveRecord::TestCase def test_validates_presence_of_non_association Boy.validates_presence_of(:name) b = Boy.new - assert b.invalid? + assert_predicate b, :invalid? b.name = "Alex" - assert b.valid? + assert_predicate b, :valid? end def test_validates_presence_of_has_one @@ -33,23 +33,23 @@ class PresenceValidationTest < ActiveRecord::TestCase b = Boy.new f = Face.new b.face = f - assert b.valid? + assert_predicate b, :valid? f.mark_for_destruction - assert b.invalid? + assert_predicate b, :invalid? end def test_validates_presence_of_has_many_marked_for_destruction Boy.validates_presence_of(:interests) b = Boy.new b.interests << [i1 = Interest.new, i2 = Interest.new] - assert b.valid? + assert_predicate b, :valid? i1.mark_for_destruction - assert b.valid? + assert_predicate b, :valid? i2.mark_for_destruction - assert b.invalid? + assert_predicate b, :invalid? end def test_validates_presence_doesnt_convert_to_array @@ -74,11 +74,11 @@ class PresenceValidationTest < ActiveRecord::TestCase Interest.validates_presence_of(:abbreviation) interest = Interest.create!(topic: "Thought Leadering", abbreviation: "tl") - assert interest.valid? + assert_predicate interest, :valid? interest.abbreviation = "" - assert interest.invalid? + assert_predicate interest, :invalid? end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index a10567f066..1fa94a5b75 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -96,7 +96,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.validates_uniqueness_of(:new_title) topic = Topic.new(new_title: "abc") - assert topic.valid? + assert_predicate topic, :valid? end def test_validates_uniqueness_with_nil_value @@ -116,7 +116,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.create!("title" => "abc") t2 = Topic.new("title" => "abc") - assert !t2.valid? + assert_not_predicate t2, :valid? assert t2.errors[:title] end @@ -259,15 +259,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1) assert !t2.valid?, "Shouldn't be valid" assert !t2.save, "Shouldn't save t2 as unique" - assert t2.errors[:title].any? - assert t2.errors[:parent_id].any? + assert_predicate t2.errors[:title], :any? + assert_predicate t2.errors[:parent_id], :any? assert_equal ["has already been taken"], t2.errors[:title] t2.title = "I'm truly UNIQUE!" assert !t2.valid?, "Shouldn't be valid" assert !t2.save, "Shouldn't save t2 as unique" - assert t2.errors[:title].empty? - assert t2.errors[:parent_id].any? + assert_empty t2.errors[:title] + assert_predicate t2.errors[:parent_id], :any? t2.parent_id = 4 assert t2.save, "Should now save t2 as unique" @@ -326,15 +326,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase t2 = Topic.new("title" => "I'M UNIQUE!") assert t2.valid?, "Should be valid" assert t2.save, "Should save t2 as unique" - assert t2.errors[:title].empty? - assert t2.errors[:parent_id].empty? + assert_empty t2.errors[:title] + assert_empty t2.errors[:parent_id] assert_not_equal ["has already been taken"], t2.errors[:title] t3 = Topic.new("title" => "I'M uNiQUe!") assert t3.valid?, "Should be valid" assert t3.save, "Should save t2 as unique" - assert t3.errors[:title].empty? - assert t3.errors[:parent_id].empty? + assert_empty t3.errors[:title] + assert_empty t3.errors[:parent_id] assert_not_equal ["has already been taken"], t3.errors[:title] end @@ -343,7 +343,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase Topic.create!("title" => 101) t2 = Topic.new("title" => 101) - assert !t2.valid? + assert_not_predicate t2, :valid? assert t2.errors[:title] end @@ -360,7 +360,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") assert t1.save t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") - assert !t2.valid? + assert_not_predicate t2, :valid? end end @@ -460,16 +460,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase def test_validate_uniqueness_on_existing_relation event = Event.create - assert TopicWithUniqEvent.create(event: event).valid? + assert_predicate TopicWithUniqEvent.create(event: event), :valid? topic = TopicWithUniqEvent.new(event: event) - assert_not topic.valid? + assert_not_predicate topic, :valid? assert_equal ["has already been taken"], topic.errors[:event] end def test_validate_uniqueness_on_empty_relation topic = TopicWithUniqEvent.new - assert topic.valid? + assert_predicate topic, :valid? end def test_validate_uniqueness_of_custom_primary_key @@ -488,7 +488,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase key2 = klass.create!(key_number: 11) key2.key_number = 10 - assert_not key2.valid? + assert_not_predicate key2, :valid? end def test_validate_uniqueness_without_primary_key @@ -501,8 +501,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase end abc = klass.create!(dashboard_id: "abc") - assert klass.new(dashboard_id: "xyz").valid? - assert_not klass.new(dashboard_id: "abc").valid? + assert_predicate klass.new(dashboard_id: "xyz"), :valid? + assert_not_predicate klass.new(dashboard_id: "abc"), :valid? abc.dashboard_id = "def" @@ -530,7 +530,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert topic.author_name.start_with?("Title1") topic2 = TopicWithAfterCreate.new(title: "Title1") - refute topic2.valid? + assert_not_predicate topic2, :valid? assert_equal(["has already been taken"], topic2.errors[:title]) end @@ -550,7 +550,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert_empty item.errors item2 = CoolTopic.new(id: item.id, title: "MyItem2") - refute item2.valid? + assert_not_predicate item2, :valid? assert_equal(["has already been taken"], item2.errors[:id]) end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 7f84939027..6c83bbd15c 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -19,7 +19,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_valid_uses_create_context_when_new r = WrongReply.new r.title = "Wrong Create" - assert_not r.valid? + assert_not_predicate r, :valid? assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error" end @@ -139,7 +139,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_throw_away_typing d = Developer.new("name" => "David", "salary" => "100,000") - assert !d.valid? + assert_not_predicate d, :valid? assert_equal 100, d.salary assert_equal "100,000", d.salary_before_type_cast end @@ -166,7 +166,7 @@ class ValidationsTest < ActiveRecord::TestCase topic = klass.new(wibble: "123-4567") topic.wibble.gsub!("-", "") - assert topic.valid? + assert_predicate topic, :valid? end def test_numericality_validation_checks_against_raw_value @@ -175,12 +175,12 @@ class ValidationsTest < ActiveRecord::TestCase ActiveModel::Name.new(self, nil, "Topic") end attribute :wibble, :decimal, scale: 2, precision: 9 - validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18") + validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal("97.18") end - assert_not klass.new(wibble: "97.179").valid? - assert_not klass.new(wibble: 97.179).valid? - assert_not klass.new(wibble: BigDecimal.new("97.179")).valid? + assert_not_predicate klass.new(wibble: "97.179"), :valid? + assert_not_predicate klass.new(wibble: 97.179), :valid? + assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid? end def test_acceptance_validator_doesnt_require_db_connection diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 578881f754..60ebdce178 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -96,7 +96,7 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_deserializing_rails_41_yaml topic = YAML.load(yaml_fixture("rails_4_1")) - assert topic.new_record? + assert_predicate topic, :new_record? assert_nil topic.id assert_equal "The First Topic", topic.title assert_equal({ omg: :lol }, topic.content) @@ -105,7 +105,7 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_deserializing_rails_4_2_0_yaml topic = YAML.load(yaml_fixture("rails_4_2_0")) - assert_not topic.new_record? + assert_not_predicate topic, :new_record? assert_equal 1, topic.id assert_equal "The First Topic", topic.title assert_equal("Have a nice day", topic.content) diff --git a/activerecord/test/fixtures/customers.yml b/activerecord/test/fixtures/customers.yml index 0399ff83b9..7d6c1366d0 100644 --- a/activerecord/test/fixtures/customers.yml +++ b/activerecord/test/fixtures/customers.yml @@ -23,4 +23,13 @@ barney: address_street: Quiet Road address_city: Peaceful Town address_country: Tranquil Land - gps_location: NULL
\ No newline at end of file + gps_location: NULL + +mary: + id: 4 + name: Mary + balance: 1 + address_street: Funny Street + address_city: Peaceful Town + address_country: Nation Land + gps_location: NULL diff --git a/activerecord/test/fixtures/minimalistics.yml b/activerecord/test/fixtures/minimalistics.yml index c3ec546209..83df0551bc 100644 --- a/activerecord/test/fixtures/minimalistics.yml +++ b/activerecord/test/fixtures/minimalistics.yml @@ -1,2 +1,5 @@ +zero: + id: 0 + first: id: 1 diff --git a/activerecord/test/fixtures/reserved_words/values.yml b/activerecord/test/fixtures/reserved_words/values.yml index 7d109609ab..9ed9e5edc5 100644 --- a/activerecord/test/fixtures/reserved_words/values.yml +++ b/activerecord/test/fixtures/reserved_words/values.yml @@ -1,7 +1,7 @@ values1: - id: 1 + as: 1 group_id: 2 values2: - id: 2 + as: 2 group_id: 1 diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml deleted file mode 100644 index ff515beb45..0000000000 --- a/activerecord/test/fixtures/teapots.yml +++ /dev/null @@ -1,3 +0,0 @@ -bob: - id: 1 - name: Bob diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index cb8686f315..27da886e1c 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -88,6 +88,9 @@ class Author < ActiveRecord::Base has_many :special_categories, through: :special_categorizations, source: :category has_one :special_category, through: :special_categorizations, source: :category + has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category + has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category + has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category" has_many :categorized_posts, through: :categorizations, source: :post diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index bbc5fc2b2d..fc6488f729 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -87,6 +87,8 @@ class Firm < Company has_many :association_with_references, -> { references(:foo) }, class_name: "Client" + has_many :developers_with_select, -> { select("id, name, first_name") }, class_name: "Developer" + has_one :lead_developer, class_name: "Developer" has_many :projects diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index 9454217e8d..f273badd85 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -2,7 +2,7 @@ class Contract < ActiveRecord::Base belongs_to :company - belongs_to :developer + belongs_to :developer, primary_key: :id belongs_to :firm, foreign_key: "company_id" before_save :hi diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 524a9d7bd9..bc501a5ce0 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -4,7 +4,7 @@ class Customer < ActiveRecord::Base cattr_accessor :gps_conversion_was_run composed_of :address, mapping: [ %w(address_street street), %w(address_city city), %w(address_country country) ], allow_nil: true - composed_of :balance, class_name: "Money", mapping: %w(balance amount), converter: Proc.new(&:to_money) + composed_of :balance, class_name: "Money", mapping: %w(balance amount) composed_of :gps_location, allow_nil: true composed_of :non_blank_gps_location, class_name: "GpsLocation", allow_nil: true, mapping: %w(gps_location gps_location), converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps) } diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 796aaa4dc9..948435136d 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -8,4 +8,8 @@ class Face < ActiveRecord::Base # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face + + validate do + man + end end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 7f064bf3dd..54eb5e6783 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -106,6 +106,9 @@ class Post < ActiveRecord::Base end end + has_many :indestructible_taggings, as: :taggable, counter_cache: :indestructible_tags_count + has_many :indestructible_tags, through: :indestructible_taggings, source: :tag + has_many :taggings_with_delete_all, class_name: "Tagging", as: :taggable, dependent: :delete_all, counter_cache: :taggings_with_delete_all_count has_many :taggings_with_destroy, class_name: "Tagging", as: :taggable, dependent: :destroy, counter_cache: :taggings_with_destroy_count @@ -319,5 +322,17 @@ class FakeKlass def arel_attribute(name, table) table[name] end + + def enforce_raw_sql_whitelist(*args) + # noop + end + + def arel_table + Post.arel_table + end + + def predicate_builder + Post.predicate_builder + end end end diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb index bc13c3a42d..c1a8890a8a 100644 --- a/activerecord/test/models/tag.rb +++ b/activerecord/test/models/tag.rb @@ -11,6 +11,6 @@ end class OrderedTag < Tag self.table_name = "tags" - has_many :taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id" - has_many :tagged_posts, through: :taggings, source: "taggable", source_type: "Post" + has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging" + has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post" end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index 861fde633f..6d4230f6f4 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -14,3 +14,7 @@ class Tagging < ActiveRecord::Base belongs_to :taggable, polymorphic: true, counter_cache: :tags_count has_many :things, through: :taggable end + +class IndestructibleTagging < Tagging + before_destroy { throw :abort } +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 2154b50ef7..8cd4dc352a 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -65,6 +65,9 @@ class Topic < ActiveRecord::Base after_initialize :set_email_address + attr_accessor :change_approved_before_save + before_save :change_approved_callback + class_attribute :after_initialize_called after_initialize do self.class.after_initialize_called = true @@ -96,6 +99,10 @@ class Topic < ActiveRecord::Base def before_destroy_for_transaction; end def after_save_for_transaction; end def after_create_for_transaction; end + + def change_approved_callback + self.approved = change_approved_before_save unless change_approved_before_save.nil? + end end class ImportantTopic < Topic diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb index e05fb64477..8db57d181e 100644 --- a/activerecord/test/models/wheel.rb +++ b/activerecord/test/models/wheel.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class Wheel < ActiveRecord::Base - belongs_to :wheelable, polymorphic: true, counter_cache: true + belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: true end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a4505a4892..7d008eecd5 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -123,7 +123,7 @@ ActiveRecord::Schema.define do create_table :cars, force: true do |t| t.string :name t.integer :engines_count - t.integer :wheels_count + t.integer :wheels_count, default: 0 t.column :lock_version, :integer, null: false, default: 0 t.timestamps null: false end @@ -205,6 +205,8 @@ ActiveRecord::Schema.define do t.bigint :rating, default: 1 t.integer :account_id t.string :description, default: "" + t.index [:name, :rating], order: :desc + t.index [:name, :description], length: 10 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 @@ -688,6 +690,7 @@ ActiveRecord::Schema.define do t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 + t.integer :indestructible_tags_count, default: 0 t.integer :tags_with_destroy_count, default: 0 t.integer :tags_with_nullify_count, default: 0 end @@ -845,6 +848,7 @@ ActiveRecord::Schema.define do t.column :taggable_type, :string t.column :taggable_id, :integer t.string :comment + t.string :type end create_table :tasks, force: true do |t| @@ -962,6 +966,7 @@ ActiveRecord::Schema.define do end create_table :wheels, force: true do |t| + t.integer :size t.references :wheelable, polymorphic: true end diff --git a/activestorage/.gitignore b/activestorage/.gitignore index a532335bdd..29bdcc4468 100644 --- a/activestorage/.gitignore +++ b/activestorage/.gitignore @@ -1,5 +1,6 @@ .byebug_history node_modules +src test/dummy/db/*.sqlite3 test/dummy/db/*.sqlite3-journal test/dummy/log/*.log diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 358552313f..682e35e0a8 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,8 @@ -* Added to Rails. +* Add source code to published npm package - *DHH* + This allows activestorage users to depend on the javascript source code + rather than the compiled code, which can produce smaller javascript bundles. + + *Richard Macklin* + +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activestorage/CHANGELOG.md) for previous changes. diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE index 4e1c6cad79..eed89ac398 100644 --- a/activestorage/MIT-LICENSE +++ b/activestorage/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 David Heinemeier Hansson, Basecamp +Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activestorage/README.md b/activestorage/README.md index 78e4463c5a..85ab70dac6 100644 --- a/activestorage/README.md +++ b/activestorage/README.md @@ -1,6 +1,6 @@ # Active Storage -Active Storage makes it simple to upload and reference files in cloud services like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage, and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage. +Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage/docs/), or [Microsoft Azure Storage](https://azure.microsoft.com/en-us/services/storage/), and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage. Files can be uploaded from the server to the cloud or directly from the client to the cloud. @@ -143,3 +143,17 @@ Active Storage, with its included JavaScript library, supports uploading directl ## License Active Storage is released under the [MIT License](https://opensource.org/licenses/MIT). + +## Support + +API documentation is at: + +* http://api.rubyonrails.org + +Bug reports for the Ruby on Rails project can be filed here: + +* https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec index 911e1a0469..d135324700 100644 --- a/activestorage/activestorage.gemspec +++ b/activestorage/activestorage.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.email = "david@loudthinking.com" s.homepage = "http://rubyonrails.org" - s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*"] + s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"] s.require_path = "lib" s.metadata = { @@ -27,4 +27,8 @@ Gem::Specification.new do |s| s.add_dependency "actionpack", version s.add_dependency "activerecord", version + + s.add_dependency "marcel", "~> 0.3.1" + + s.add_development_dependency "webmock", "~> 3.2.1" end diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index c1c0a2f6d9..a2f06c43a3 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=r.bubbles,i=r.cancelable,a=r.detail,u=document.createEvent("Event");return u.initEvent(e,n||!0,i||!0),u.detail=a||{},t.dispatchEvent(u),u}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;if(r>=200&&r<300){var i=n.direct_upload;delete n.direct_upload,this.attributes=n,this.directUploadData=i,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.xhr.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file)}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])});
\ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file)}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])});
\ No newline at end of file diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb index a17e3852f9..fa44131048 100644 --- a/activestorage/app/controllers/active_storage/blobs_controller.rb +++ b/activestorage/app/controllers/active_storage/blobs_controller.rb @@ -5,12 +5,10 @@ # security-through-obscurity factor of the signed blob references, you'll need to implement your own # authenticated redirection controller. class ActiveStorage::BlobsController < ActionController::Base + include ActiveStorage::SetBlob + def show - if blob = ActiveStorage::Blob.find_signed(params[:signed_id]) - expires_in ActiveStorage::Blob.service.url_expires_in - redirect_to blob.service_url(disposition: params[:disposition]) - else - head :not_found - end + expires_in ActiveStorage::Blob.service.url_expires_in + redirect_to @blob.service_url(disposition: params[:disposition]) end end diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index a4fd427cb2..a7e10c0696 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -5,6 +5,8 @@ # Always go through the BlobsController, or your own authenticated controller, rather than directly # to the service url. class ActiveStorage::DiskController < ActionController::Base + skip_forgery_protection if default_protect_from_forgery + def show if key = decode_verified_key send_data disk_service.download(key), diff --git a/activestorage/app/controllers/active_storage/previews_controller.rb b/activestorage/app/controllers/active_storage/previews_controller.rb index 9e8cf27b6e..aa7ef58ca4 100644 --- a/activestorage/app/controllers/active_storage/previews_controller.rb +++ b/activestorage/app/controllers/active_storage/previews_controller.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true class ActiveStorage::PreviewsController < ActionController::Base + include ActiveStorage::SetBlob + def show - if blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id]) - expires_in ActiveStorage::Blob.service.url_expires_in - redirect_to ActiveStorage::Preview.new(blob, params[:variation_key]).processed.service_url(disposition: params[:disposition]) - else - head :not_found - end + expires_in ActiveStorage::Blob.service.url_expires_in + redirect_to ActiveStorage::Preview.new(@blob, params[:variation_key]).processed.service_url(disposition: params[:disposition]) end end diff --git a/activestorage/app/controllers/active_storage/variants_controller.rb b/activestorage/app/controllers/active_storage/variants_controller.rb index dc5e78ecc0..e8f8dd592d 100644 --- a/activestorage/app/controllers/active_storage/variants_controller.rb +++ b/activestorage/app/controllers/active_storage/variants_controller.rb @@ -5,12 +5,10 @@ # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own # authenticated redirection controller. class ActiveStorage::VariantsController < ActionController::Base + include ActiveStorage::SetBlob + def show - if blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id]) - expires_in ActiveStorage::Blob.service.url_expires_in - redirect_to ActiveStorage::Variant.new(blob, params[:variation_key]).processed.service_url(disposition: params[:disposition]) - else - head :not_found - end + expires_in ActiveStorage::Blob.service.url_expires_in + redirect_to ActiveStorage::Variant.new(@blob, params[:variation_key]).processed.service_url(disposition: params[:disposition]) end end diff --git a/activestorage/app/controllers/concerns/active_storage/set_blob.rb b/activestorage/app/controllers/concerns/active_storage/set_blob.rb new file mode 100644 index 0000000000..f072954d78 --- /dev/null +++ b/activestorage/app/controllers/concerns/active_storage/set_blob.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module ActiveStorage::SetBlob #:nodoc: + extend ActiveSupport::Concern + + included do + before_action :set_blob + end + + private + def set_blob + @blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + head :not_found + end +end diff --git a/activestorage/app/javascript/activestorage/blob_record.js b/activestorage/app/javascript/activestorage/blob_record.js index 3c6e6b6ba1..ff847892b2 100644 --- a/activestorage/app/javascript/activestorage/blob_record.js +++ b/activestorage/app/javascript/activestorage/blob_record.js @@ -22,14 +22,28 @@ export class BlobRecord { this.xhr.addEventListener("error", event => this.requestDidError(event)) } + get status() { + return this.xhr.status + } + + get response() { + const { responseType, response } = this.xhr + if (responseType == "json") { + return response + } else { + // Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808 + return JSON.parse(response) + } + } + create(callback) { this.callback = callback this.xhr.send(JSON.stringify({ blob: this.attributes })) } requestDidLoad(event) { - const { status, response } = this.xhr - if (status >= 200 && status < 300) { + if (this.status >= 200 && this.status < 300) { + const { response } = this const { direct_upload } = response delete response.direct_upload this.attributes = response @@ -41,7 +55,7 @@ export class BlobRecord { } requestDidError(event) { - this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.xhr.status}`) + this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`) } toJSON() { diff --git a/activestorage/app/javascript/activestorage/helpers.js b/activestorage/app/javascript/activestorage/helpers.js index 52fec8f6f1..7e83c447e7 100644 --- a/activestorage/app/javascript/activestorage/helpers.js +++ b/activestorage/app/javascript/activestorage/helpers.js @@ -23,11 +23,20 @@ export function findElement(root, selector) { } export function dispatchEvent(element, type, eventInit = {}) { + const { disabled } = element const { bubbles, cancelable, detail } = eventInit const event = document.createEvent("Event") + event.initEvent(type, bubbles || true, cancelable || true) event.detail = detail || {} - element.dispatchEvent(event) + + try { + element.disabled = false + element.dispatchEvent(event) + } finally { + element.disabled = disabled + } + return event } diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 9f61a5dbf3..19f48c57d6 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -14,7 +14,7 @@ class ActiveStorage::Attachment < ActiveRecord::Base delegate_missing_to :blob - after_create_commit :analyze_blob_later + after_create_commit :identify_blob, :analyze_blob_later # Synchronously purges the blob (deletes it from the configured service) and destroys the attachment. def purge @@ -29,6 +29,10 @@ class ActiveStorage::Attachment < ActiveRecord::Base end private + def identify_blob + blob.identify + end + def analyze_blob_later blob.analyze_later unless blob.analyzed? end diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 99823e14c6..a1e69e2264 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_storage/analyzer/null_analyzer" - # A blob is a record that contains the metadata about a file and a key for where that file resides on the service. # Blobs can be created in two ways: # @@ -16,20 +14,17 @@ require "active_storage/analyzer/null_analyzer" # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file. # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. class ActiveStorage::Blob < ActiveRecord::Base - class UnpreviewableError < StandardError; end - class UnrepresentableError < StandardError; end + include Analyzable, Identifiable, Representable self.table_name = "active_storage_blobs" has_secure_token :key - store :metadata, accessors: [ :analyzed ], coder: JSON + store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON class_attribute :service has_many :attachments - has_one_attached :preview_image - 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 @@ -68,7 +63,6 @@ class ActiveStorage::Blob < ActiveRecord::Base end end - # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose. def signed_id @@ -110,85 +104,16 @@ class ActiveStorage::Blob < ActiveRecord::Base content_type.start_with?("text") end - # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image - # files, and it allows any image to be transformed for size, colors, and the like. Example: - # - # avatar.variant(resize: "100x100").processed.service_url - # - # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px. - # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. - # - # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a - # specific variant that can be created by a controller on-demand. Like so: - # - # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> - # - # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController - # can then produce on-demand. - def variant(transformations) - ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations)) - end - - - # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated - # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer - # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document. - # - # blob.preview(resize: "100x100").processed.service_url - # - # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand. - # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s - # how to use the built-in version: - # - # <%= image_tag video.preview(resize: "100x100") %> - # - # This method raises ActiveStorage::Blob::UnpreviewableError if no previewer accepts the receiving blob. To determine - # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. - def preview(transformations) - if previewable? - ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations)) - else - raise UnpreviewableError - end - end - - # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents. - def previewable? - ActiveStorage.previewers.any? { |klass| klass.accept?(self) } - end - - - # Returns an ActiveStorage::Preview instance for a previewable blob or an ActiveStorage::Variant instance for an image blob. - # - # blob.representation(resize: "100x100").processed.service_url - # - # Raises ActiveStorage::Blob::UnrepresentableError if the receiving blob is neither an image nor previewable. Call - # ActiveStorage::Blob#representable? to determine whether a blob is representable. - # - # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information. - def representation(transformations) - case - when previewable? - preview transformations - when image? - variant transformations - else - raise UnrepresentableError - end - end - - # Returns true if the blob is an image or is previewable. - def representable? - image? || previewable? - end - # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL. # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And # it allows permanent URLs that redirect to the +service_url+ to be cached in the view. - def service_url(expires_in: service.url_expires_in, disposition: "inline") - service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type + def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options) + filename = ActiveStorage::Filename.wrap(filename || self.filename) + + service.url key, expires_in: expires_in, filename: filename, content_type: content_type, + disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options end # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be @@ -202,6 +127,7 @@ class ActiveStorage::Blob < ActiveRecord::Base service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum end + # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob, # you should instead simply create a new blob based on the old one. @@ -213,8 +139,10 @@ class ActiveStorage::Blob < ActiveRecord::Base # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+ # and +create_after_upload!+. def upload(io) - self.checksum = compute_checksum_in_chunks(io) - self.byte_size = io.size + self.checksum = compute_checksum_in_chunks(io) + self.content_type = extract_content_type(io) + self.byte_size = io.size + self.identified = true service.upload(key, io, checksum: checksum) end @@ -226,51 +154,12 @@ class ActiveStorage::Blob < ActiveRecord::Base end - # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes - # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and - # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party - # libraries they require. - # - # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the - # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no - # metadata is extracted from it. - # - # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+ - # in an initializer: - # - # # Add a custom analyzer for Microsoft Office documents: - # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer - # - # # Remove the built-in video analyzer: - # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer - # - # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead. - # - # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously - # analyzed via #analyze_later when they're attached for the first time. - def analyze - update! metadata: extract_metadata_via_analyzer - end - - # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze. - # - # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob - # again (e.g. if you add a new analyzer or modify an existing one). - def analyze_later - ActiveStorage::AnalyzeJob.perform_later(self) - end - - # Returns true if the blob has been analyzed. - def analyzed? - analyzed - end - - # Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+ # methods in most circumstances. def delete - service.delete key + service.delete(key) + service.delete_prefixed("variants/#{key}/") if image? end # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted @@ -298,16 +187,11 @@ class ActiveStorage::Blob < ActiveRecord::Base end.base64digest end - - def extract_metadata_via_analyzer - analyzer.metadata.merge(analyzed: true) - end - - def analyzer - analyzer_class.new(self) + def extract_content_type(io) + Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type end - def analyzer_class - ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer + def forcibly_serve_as_binary? + ActiveStorage.content_types_to_serve_as_binary.include?(content_type) end end diff --git a/activestorage/app/models/active_storage/blob/analyzable.rb b/activestorage/app/models/active_storage/blob/analyzable.rb new file mode 100644 index 0000000000..5bda6e6d73 --- /dev/null +++ b/activestorage/app/models/active_storage/blob/analyzable.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "active_storage/analyzer/null_analyzer" + +module ActiveStorage::Blob::Analyzable + # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes + # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and + # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party + # libraries they require. + # + # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the + # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no + # metadata is extracted from it. + # + # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+ + # in an initializer: + # + # # Add a custom analyzer for Microsoft Office documents: + # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer + # + # # Remove the built-in video analyzer: + # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer + # + # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead. + # + # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously + # analyzed via #analyze_later when they're attached for the first time. + def analyze + update! metadata: metadata.merge(extract_metadata_via_analyzer) + end + + # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze. + # + # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob + # again (e.g. if you add a new analyzer or modify an existing one). + def analyze_later + ActiveStorage::AnalyzeJob.perform_later(self) + end + + # Returns true if the blob has been analyzed. + def analyzed? + analyzed + end + + private + def extract_metadata_via_analyzer + analyzer.metadata.merge(analyzed: true) + end + + def analyzer + analyzer_class.new(self) + end + + def analyzer_class + ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer + end +end diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb new file mode 100644 index 0000000000..40ca84ac70 --- /dev/null +++ b/activestorage/app/models/active_storage/blob/identifiable.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ActiveStorage::Blob::Identifiable + def identify + ActiveStorage::Identification.new(self).apply + end + + def identified? + identified + end +end diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb new file mode 100644 index 0000000000..0ad2e2fd77 --- /dev/null +++ b/activestorage/app/models/active_storage/blob/representable.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module ActiveStorage::Blob::Representable + extend ActiveSupport::Concern + + included do + has_one_attached :preview_image + end + + # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image + # files, and it allows any image to be transformed for size, colors, and the like. Example: + # + # avatar.variant(resize: "100x100").processed.service_url + # + # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px. + # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. + # + # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a + # specific variant that can be created by a controller on-demand. Like so: + # + # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> + # + # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController + # can then produce on-demand. + # + # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is + # variable, call ActiveStorage::Blob#variable?. + def variant(transformations) + if variable? + ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations)) + else + raise ActiveStorage::InvariableError + end + end + + # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+). + def variable? + ActiveStorage.variable_content_types.include?(content_type) + end + + + # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated + # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer + # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document. + # + # blob.preview(resize: "100x100").processed.service_url + # + # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand. + # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s + # how to use the built-in version: + # + # <%= image_tag video.preview(resize: "100x100") %> + # + # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine + # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. + def preview(transformations) + if previewable? + ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations)) + else + raise ActiveStorage::UnpreviewableError + end + end + + # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents. + def previewable? + ActiveStorage.previewers.any? { |klass| klass.accept?(self) } + end + + + # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob. + # + # blob.representation(resize: "100x100").processed.service_url + # + # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call + # ActiveStorage::Blob#representable? to determine whether a blob is representable. + # + # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information. + def representation(transformations) + case + when previewable? + preview transformations + when variable? + variant transformations + else + raise ActiveStorage::UnrepresentableError + end + end + + # Returns true if the blob is variable or previewable. + def representable? + variable? || previewable? + end +end diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb index 79d55dc889..2b8880716e 100644 --- a/activestorage/app/models/active_storage/filename.rb +++ b/activestorage/app/models/active_storage/filename.rb @@ -5,6 +5,14 @@ class ActiveStorage::Filename include Comparable + class << self + # Returns a Filename instance based on the given filename. If the filename is a Filename, it is + # returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new. + def wrap(filename) + filename.kind_of?(self) ? filename : new(filename) + end + end + def initialize(filename) @filename = filename end @@ -50,7 +58,7 @@ class ActiveStorage::Filename @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-") end - def parameters + def parameters #:nodoc: Parameters.new self end diff --git a/activestorage/app/models/active_storage/filename/parameters.rb b/activestorage/app/models/active_storage/filename/parameters.rb index 58ce198d38..fb9ea10e49 100644 --- a/activestorage/app/models/active_storage/filename/parameters.rb +++ b/activestorage/app/models/active_storage/filename/parameters.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActiveStorage::Filename::Parameters +class ActiveStorage::Filename::Parameters #:nodoc: attr_reader :filename def initialize(filename) diff --git a/activestorage/app/models/active_storage/identification.rb b/activestorage/app/models/active_storage/identification.rb new file mode 100644 index 0000000000..4f295257ae --- /dev/null +++ b/activestorage/app/models/active_storage/identification.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class ActiveStorage::Identification + attr_reader :blob + + def initialize(blob) + @blob = blob + end + + def apply + blob.update!(content_type: content_type, identified: true) unless blob.identified? + end + + private + def content_type + Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type) + end + + + def identifiable_chunk + Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client| + client.get(uri, "Range" => "0-4096").body + end + end + + def uri + @uri ||= URI.parse(blob.service_url) + end + + + def filename + blob.filename.to_s + end + + def declared_content_type + blob.content_type + end +end diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index fa5aa69bd3..e08a2271ec 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 "active_storage/downloading" + # 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. @@ -16,7 +18,7 @@ # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # -# <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %> +# <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. @@ -35,6 +37,10 @@ # # avatar.variant(resize: "100x100", monochrome: true, flip: "-90") class ActiveStorage::Variant + include ActiveStorage::Downloading + + WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif ) + attr_reader :blob, :variation delegate :service, to: :blob @@ -62,7 +68,7 @@ class ActiveStorage::Variant # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method # for its redirection. def service_url(expires_in: service.url_expires_in, disposition: :inline) - service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type + service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type end # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably. @@ -76,11 +82,51 @@ class ActiveStorage::Variant end def process - service.upload key, transform(service.download(blob.key)) + open_image do |image| + transform image + format image + upload image + end + end + + + def filename + if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) + blob.filename + else + ActiveStorage::Filename.new("#{blob.filename.base}.png") + end + end + + def content_type + blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png" + end + + + def open_image(&block) + image = download_image + + begin + yield image + ensure + image.destroy! + end end - def transform(io) + def download_image require "mini_magick" - File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path + MiniMagick::Image.create { |file| download_blob_to(file) } + end + + def transform(image) + variation.transform(image) + end + + def format(image) + image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) + end + + def upload(image) + File.open(image.path, "r") { |file| service.upload(key, file) } end end diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index 13bad87cac..12e7f9f0b5 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -8,6 +8,14 @@ # # ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90") # +# You can also combine multiple transformations in one step, e.g. for center-weighted cropping: +# +# ActiveStorage::Variation.new(combine_options: { +# resize: "100x100^", +# gravity: "center", +# crop: "100x100+0+0", +# }) +# # A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php. class ActiveStorage::Variation attr_reader :transformations @@ -46,11 +54,17 @@ class ActiveStorage::Variation # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>, # and performs the +transformations+ against it. The transformed image instance is then returned. def transform(image) - transformations.each do |(method, argument)| - if eligible_argument?(argument) - image.public_send(method, argument) - else - image.public_send(method) + ActiveSupport::Notifications.instrument("transform.active_storage") do + 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 end end @@ -61,6 +75,14 @@ class ActiveStorage::Variation end private + def pass_transform_argument(command, method, argument) + if eligible_argument?(argument) + command.public_send(method, argument) + else + command.public_send(method) + end + end + def eligible_argument?(argument) argument.present? && argument != true end diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb index c659e079fd..3f4129d915 100644 --- a/activestorage/config/routes.rb +++ b/activestorage/config/routes.rb @@ -1,43 +1,43 @@ # 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, internal: true + get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob - direct :rails_blob do |blob| - route_for(:rails_service_blob, blob.signed_id, blob.filename) + direct :rails_blob do |blob, options| + route_for(:rails_service_blob, blob.signed_id, blob.filename, options) end - resolve("ActiveStorage::Blob") { |blob| route_for(:rails_blob, blob) } - resolve("ActiveStorage::Attachment") { |attachment| route_for(:rails_blob, attachment.blob) } + resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) } + resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) } - get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation, internal: true + get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation - direct :rails_variant do |variant| + direct :rails_variant do |variant, options| signed_blob_id = variant.blob.signed_id variation_key = variant.variation.key filename = variant.blob.filename - route_for(:rails_blob_variation, signed_blob_id, variation_key, filename) + route_for(:rails_blob_variation, signed_blob_id, variation_key, filename, options) end - resolve("ActiveStorage::Variant") { |variant| route_for(:rails_variant, variant) } + resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_variant, variant, options) } - get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview, internal: true + get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview - direct :rails_preview do |preview| + direct :rails_preview do |preview, options| signed_blob_id = preview.blob.signed_id variation_key = preview.variation.key filename = preview.blob.filename - route_for(:rails_blob_preview, signed_blob_id, variation_key, filename) + route_for(:rails_blob_preview, signed_blob_id, variation_key, filename, options) end - resolve("ActiveStorage::Preview") { |preview| route_for(:rails_preview, preview) } + resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_preview, preview, options) } - get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service, internal: true - put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service, internal: true - post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads, internal: true + 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 end diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index d1ff6b7032..e1bd974853 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2017 David Heinemeier Hansson +# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -26,7 +26,11 @@ require "active_record" require "active_support" require "active_support/rails" + require "active_storage/version" +require "active_storage/errors" + +require "marcel" module ActiveStorage extend ActiveSupport::Autoload @@ -41,4 +45,7 @@ module ActiveStorage mattr_accessor :queue mattr_accessor :previewers, default: [] mattr_accessor :analyzers, default: [] + mattr_accessor :paths, default: {} + mattr_accessor :variable_content_types, default: [] + mattr_accessor :content_types_to_serve_as_binary, default: [] end diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb index 837785a12b..7c4168c1a0 100644 --- a/activestorage/lib/active_storage/analyzer.rb +++ b/activestorage/lib/active_storage/analyzer.rb @@ -26,7 +26,7 @@ module ActiveStorage end private - def logger + def logger #:doc: ActiveStorage.logger end end diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb index 25e0251e6e..3b39de91be 100644 --- a/activestorage/lib/active_storage/analyzer/image_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb @@ -3,14 +3,15 @@ module ActiveStorage # Extracts width and height in pixels from an image blob. # + # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience. + # # Example: # # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata # # => { width: 4104, height: 2736 } # # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires - # the {ImageMagick}[http://www.imagemagick.org] system library. These libraries are not provided by Rails; you must - # install them yourself to use this analyzer. + # the {ImageMagick}[http://www.imagemagick.org] system library. class Analyzer::ImageAnalyzer < Analyzer def self.accept?(blob) blob.image? @@ -18,7 +19,11 @@ module ActiveStorage def metadata read_image do |image| - { width: image.width, height: image.height } + if rotated_image?(image) + { width: image.height, height: image.width } + else + { width: image.width, height: image.height } + end end rescue LoadError logger.info "Skipping image analysis because the mini_magick gem isn't installed" @@ -32,5 +37,9 @@ module ActiveStorage yield MiniMagick::Image.new(file.path) end end + + def rotated_image?(image) + %w[ RightTop LeftBottom ].include?(image["%[orientation]"]) + end end end diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb index 408b5e58e9..656e362187 100644 --- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb @@ -9,31 +9,40 @@ module ActiveStorage # * Height (pixels) # * Duration (seconds) # * Angle (degrees) - # * Aspect ratio + # * Display aspect ratio # # Example: # # ActiveStorage::VideoAnalyzer.new(blob).metadata - # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] } + # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] } # - # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must - # install ffmpeg yourself to use this analyzer. + # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience. + # + # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. class Analyzer::VideoAnalyzer < Analyzer def self.accept?(blob) blob.video? end def metadata - { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact + { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact end private def width - Integer(video_stream["width"]) if video_stream["width"] + if rotated? + computed_height || encoded_height + else + encoded_width + end end def height - Integer(video_stream["height"]) if video_stream["height"] + if rotated? + encoded_width + else + computed_height || encoded_height + end end def duration @@ -44,12 +53,40 @@ module ActiveStorage Integer(tags["rotate"]) if tags["rotate"] end - def aspect_ratio + def display_aspect_ratio if descriptor = video_stream["display_aspect_ratio"] - descriptor.split(":", 2).collect(&:to_i) + if terms = descriptor.split(":", 2) + numerator = Integer(terms[0]) + denominator = Integer(terms[1]) + + [numerator, denominator] unless numerator == 0 + end + end + end + + + def rotated? + angle == 90 || angle == 270 + end + + def computed_height + if encoded_width && display_height_scale + encoded_width * display_height_scale end end + def encoded_width + @encoded_width ||= Float(video_stream["width"]) if video_stream["width"] + end + + def encoded_height + @encoded_height ||= Float(video_stream["height"]) if video_stream["height"] + end + + def display_height_scale + @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio + end + def tags @tags ||= video_stream["tags"] || {} @@ -68,12 +105,16 @@ module ActiveStorage end def probe_from(file) - IO.popen([ "ffprobe", "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output| + IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output| JSON.parse(output.read) end rescue Errno::ENOENT logger.info "Skipping video analysis because ffmpeg isn't installed" {} end + + def ffprobe_path + ActiveStorage.paths[:ffprobe] || "ffprobe" + end end end diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb index f0256718ac..c51efa9d6b 100644 --- a/activestorage/lib/active_storage/attached/macros.rb +++ b/activestorage/lib/active_storage/attached/macros.rb @@ -32,15 +32,19 @@ module ActiveStorage def #{name} @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}) end + + def #{name}=(attachable) + #{name}.attach(attachable) + end CODE - has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record + has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) } if dependent == :purge_later - before_destroy { public_send(name).purge_later } + after_destroy_commit { public_send(name).purge_later } end end @@ -73,15 +77,19 @@ module ActiveStorage def #{name} @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}) end + + def #{name}=(attachables) + #{name}.attach(attachables) + end CODE - has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment" + has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } if dependent == :purge_later - before_destroy { public_send(name).purge_later } + after_destroy_commit { public_send(name).purge_later } end end end diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index 1e0657c33c..6eace65b79 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -13,7 +13,6 @@ module ActiveStorage end # Associates one or several attachments with the current record, saving them to the database. - # Examples: # # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload @@ -21,7 +20,11 @@ module ActiveStorage # document.images.attach([ first_blob, second_blob ]) def attach(*attachables) attachables.flatten.collect do |attachable| - attachments.create!(name: name, blob: create_blob_from(attachable)) + if record.new_record? + attachments.build(record: record, blob: create_blob_from(attachable)) + else + attachments.create!(record: record, blob: create_blob_from(attachable)) + end end end @@ -36,6 +39,11 @@ module ActiveStorage attachments.any? end + # Deletes associated attachments without purging them, leaving their respective blobs in place. + def detach + attachments.destroy_all if attached? + end + # Directly purges each associated attachment (i.e. destroys the blobs and # attachments and deletes the files on the service). def purge diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb index dc19512484..0244232b2c 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -14,7 +14,6 @@ module ActiveStorage end # Associates a given attachment with the current record, saving it to the database. - # Examples: # # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload @@ -24,7 +23,7 @@ module ActiveStorage if attached? && dependent == :purge_later replace attachable else - write_attachment create_attachment_from(attachable) + write_attachment build_attachment_from(attachable) end end @@ -39,6 +38,14 @@ module ActiveStorage attachment.present? end + # Deletes the attachment without purging it, leaving its blob in place. + def detach + if attached? + attachment.destroy + write_attachment nil + end + end + # Directly purges the attachment (i.e. destroys the blob and # attachment and deletes the file on the service). def purge @@ -59,18 +66,14 @@ module ActiveStorage def replace(attachable) blob.tap do transaction do - destroy_attachment - write_attachment create_attachment_from(attachable) + detach + write_attachment build_attachment_from(attachable) end end.purge_later end - def destroy_attachment - attachment.destroy - end - - def create_attachment_from(attachable) - ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable)) + def build_attachment_from(attachable) + ActiveStorage::Attachment.new(record: record, name: name, blob: create_blob_from(attachable)) end def write_attachment(attachment) diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb index 3dac6b116a..f2a1fffdcb 100644 --- a/activestorage/lib/active_storage/downloading.rb +++ b/activestorage/lib/active_storage/downloading.rb @@ -1,25 +1,37 @@ # frozen_string_literal: true +require "tmpdir" + module ActiveStorage module Downloading private # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile. - def download_blob_to_tempfile # :doc: - Tempfile.open("ActiveStorage", tempdir) do |file| + def download_blob_to_tempfile #:doc: + open_tempfile_for_blob do |file| download_blob_to file yield file end end + def open_tempfile_for_blob + tempfile = Tempfile.open([ "ActiveStorage", blob.filename.extension_with_delimiter ], tempdir) + + begin + yield tempfile + ensure + tempfile.close! + end + end + # Efficiently downloads blob data into the given file. - def download_blob_to(file) # :doc: + def download_blob_to(file) #:doc: file.binmode blob.download { |chunk| file.write(chunk) } file.rewind end # Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+. - def tempdir # :doc: + def tempdir #:doc: Dir.tmpdir end end diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 6cf6635c4f..8ba32490b1 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -15,7 +15,20 @@ module ActiveStorage config.active_storage = ActiveSupport::OrderedOptions.new config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] - config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] + config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] + config.active_storage.paths = ActiveSupport::OrderedOptions.new + + config.active_storage.variable_content_types = %w( image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop ) + config.active_storage.content_types_to_serve_as_binary = %w( + text/html + text/javascript + image/svg+xml + application/postscript + application/x-shockwave-flash + text/xml + application/xml + application/xhtml+xml + ) config.eager_load_namespaces << ActiveStorage @@ -25,6 +38,10 @@ module ActiveStorage ActiveStorage.queue = app.config.active_storage.queue ActiveStorage.previewers = app.config.active_storage.previewers || [] ActiveStorage.analyzers = app.config.active_storage.analyzers || [] + ActiveStorage.paths = app.config.active_storage.paths || {} + + 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 || [] end end diff --git a/activestorage/lib/active_storage/errors.rb b/activestorage/lib/active_storage/errors.rb new file mode 100644 index 0000000000..f099b13f5b --- /dev/null +++ b/activestorage/lib/active_storage/errors.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActiveStorage + class InvariableError < StandardError; end + class UnpreviewableError < StandardError; end + class UnrepresentableError < StandardError; end +end diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb index e1d7b3493a..492620731b 100644 --- a/activestorage/lib/active_storage/gem_version.rb +++ b/activestorage/lib/active_storage/gem_version.rb @@ -7,8 +7,8 @@ module ActiveStorage end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb index 5cbf4bd1a5..a4e148c1a5 100644 --- a/activestorage/lib/active_storage/log_subscriber.rb +++ b/activestorage/lib/active_storage/log_subscriber.rb @@ -18,6 +18,10 @@ module ActiveStorage info event, color("Deleted file from key: #{key_in(event)}", RED) end + def service_delete_prefixed(event) + info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED) + end + def service_exist(event) debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE) end diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb index ed75bae3b5..cf19987d72 100644 --- a/activestorage/lib/active_storage/previewer.rb +++ b/activestorage/lib/active_storage/previewer.rb @@ -42,17 +42,33 @@ module ActiveStorage # end # # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir. - def draw(*argv) # :doc: - Tempfile.open("ActiveStorage", tempdir) do |file| - capture(*argv, to: file) - yield file + def draw(*argv) #:doc: + ActiveSupport::Notifications.instrument("preview.active_storage") do + open_tempfile_for_drawing do |file| + capture(*argv, to: file) + yield file + end + end + end + + def open_tempfile_for_drawing + tempfile = Tempfile.open("ActiveStorage", tempdir) + + begin + yield tempfile + ensure + tempfile.close! end end def capture(*argv, to:) to.binmode - IO.popen(argv) { |out| IO.copy_stream(out, to) } + IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) } to.rewind end + + def logger #:doc: + ActiveStorage.logger + end end end diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb index a2f05c74a6..426ff51eb1 100644 --- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb +++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb @@ -8,10 +8,19 @@ module ActiveStorage def preview download_blob_to_tempfile do |input| - draw "mutool", "draw", "-F", "png", "-o", "-", input.path, "1" do |output| + draw_first_page_from input do |output| yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" end end end + + private + def draw_first_page_from(file, &block) + draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block + end + + def mutool_path + ActiveStorage.paths[:mutool] || "mutool" + end end end diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb index 49f128d142..2f28a3d341 100644 --- a/activestorage/lib/active_storage/previewer/video_previewer.rb +++ b/activestorage/lib/active_storage/previewer/video_previewer.rb @@ -16,8 +16,12 @@ module ActiveStorage private def draw_relevant_frame_from(file, &block) - draw "ffmpeg", "-i", file.path, "-y", "-vcodec", "png", + draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png", "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block end + + def ffmpeg_path + ActiveStorage.paths[:ffmpeg] || "ffmpeg" + end end end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index aa150e4d8a..f2e1269f27 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -78,6 +78,11 @@ module ActiveStorage raise NotImplementedError end + # Delete files at keys starting with the +prefix+. + def delete_prefixed(prefix) + raise NotImplementedError + end + # Return +true+ if a file exists at the +key+. def exist?(key) raise NotImplementedError @@ -92,7 +97,7 @@ module ActiveStorage # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+. # The URL will be valid for the amount of seconds specified in +expires_in+. - # You most also provide the +content_type+, +content_length+, and +checksum+ of the file + # You must also provide the +content_type+, +content_length+, and +checksum+ of the file # that will be uploaded. All these attributes will be validated by the service upon upload. def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) raise NotImplementedError @@ -104,10 +109,10 @@ module ActiveStorage end private - def instrument(operation, key, payload = {}, &block) + def instrument(operation, payload = {}, &block) ActiveSupport::Notifications.instrument( "service_#{operation}.active_storage", - payload.merge(key: key, service: service_name), &block) + payload.merge(service: service_name), &block) end def service_name diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index f3877ad9c9..0a9eb7f23d 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -19,7 +19,7 @@ module ActiveStorage end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do begin blobs.create_block_blob(container, key, io, content_md5: checksum) rescue Azure::Core::Http::HTTPError @@ -30,11 +30,11 @@ module ActiveStorage def download(key, &block) if block_given? - instrument :streaming_download, key do + instrument :streaming_download, key: key do stream(key, &block) end else - instrument :download, key do + instrument :download, key: key do _, io = blobs.get_blob(container, key) io.force_encoding(Encoding::BINARY) end @@ -42,17 +42,33 @@ module ActiveStorage end def delete(key) - instrument :delete, key do + instrument :delete, key: key do begin blobs.delete_blob(container, key) rescue Azure::Core::Http::HTTPError - false + # Ignore files already deleted + end + end + end + + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + marker = nil + + loop do + results = blobs.list_blobs(container, prefix: prefix, marker: marker) + + results.each do |blob| + blobs.delete_blob(container, blob.name) + end + + break unless marker = results.continuation_token.presence end end end def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = blob_for(key).present? payload[:exist] = answer answer @@ -60,7 +76,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, disposition:, content_type:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| base_url = url_for(key) generated_url = signer.signed_uri( URI(base_url), false, @@ -77,7 +93,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| base_url = url_for(key) generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw", expiry: format_expiry(expires_in)).to_s diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index 52eaba4e7b..d17eea9046 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -9,14 +9,14 @@ module ActiveStorage # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API # documentation that applies to all services. class Service::DiskService < Service - attr_reader :root + attr_reader :root, :host - def initialize(root:) - @root = root + def initialize(root:, host: "http://localhost:3000") + @root, @host = root, host end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do IO.copy_stream(io, make_path_for(key)) ensure_integrity_of(key, checksum) if checksum end @@ -24,7 +24,7 @@ module ActiveStorage def download(key) if block_given? - instrument :streaming_download, key do + instrument :streaming_download, key: key do File.open(path_for(key), "rb") do |file| while data = file.read(64.kilobytes) yield data @@ -32,14 +32,14 @@ module ActiveStorage end end else - instrument :download, key do + instrument :download, key: key do File.binread path_for(key) end end end def delete(key) - instrument :delete, key do + instrument :delete, key: key do begin File.delete path_for(key) rescue Errno::ENOENT @@ -48,8 +48,16 @@ module ActiveStorage end end + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + Dir.glob(path_for("#{prefix}*")).each do |path| + FileUtils.rm_rf(path) + end + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = File.exist? path_for(key) payload[:exist] = answer answer @@ -57,18 +65,17 @@ module ActiveStorage end def url(key, expires_in:, filename:, disposition:, content_type:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) generated_url = - if defined?(Rails.application) - Rails.application.routes.url_helpers.rails_disk_service_path \ - verified_key_with_expiration, - filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type - else - "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}" \ - "&disposition=#{content_disposition_with(type: disposition, filename: filename)}" - end + Rails.application.routes.url_helpers.rails_disk_service_url( + verified_key_with_expiration, + filename: filename, + disposition: content_disposition_with(type: disposition, filename: filename), + content_type: content_type, + host: host + ) payload[:url] = generated_url @@ -77,7 +84,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| verified_token_with_expiration = ActiveStorage.verifier.generate( { key: key, @@ -89,12 +96,7 @@ module ActiveStorage purpose: :blob_token } ) - generated_url = - if defined?(Rails.application) - Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration - else - "/rails/active_storage/disk/#{verified_token_with_expiration}" - end + generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host) payload[:url] = generated_url diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index b4ffeeeb8a..6f6f4105fe 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +gem "google-cloud-storage", "~> 1.8" + require "google/cloud/storage" require "active_support/core_ext/object/to_query" @@ -7,17 +9,20 @@ module ActiveStorage # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API # documentation that applies to all services. class Service::GCSService < Service - attr_reader :client, :bucket - - def initialize(project:, keyfile:, bucket:, **options) - @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile, **options) - @bucket = @client.bucket(bucket) + def initialize(**config) + @config = config end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do begin - bucket.create_file(io, key, md5: checksum) + # The official GCS client library doesn't allow us to create a file with no Content-Type metadata. + # We need the file we create to have no Content-Type so we can control it via the response-content-type + # param in signed URLs. Workaround: let the GCS client create the file with an inferred + # Content-Type (usually "application/octet-stream") then clear it. + bucket.create_file(io, key, md5: checksum).update do |file| + file.content_type = nil + end rescue Google::Cloud::InvalidArgumentError raise ActiveStorage::IntegrityError end @@ -26,7 +31,7 @@ module ActiveStorage # FIXME: Download in chunks when given a block. def download(key) - instrument :download, key do + instrument :download, key: key do io = file_for(key).download io.rewind @@ -39,7 +44,7 @@ module ActiveStorage end def delete(key) - instrument :delete, key do + instrument :delete, key: key do begin file_for(key).delete rescue Google::Cloud::NotFoundError @@ -48,8 +53,14 @@ module ActiveStorage end end + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + bucket.files(prefix: prefix).all(&:delete) + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = file_for(key).exists? payload[:exist] = answer answer @@ -57,7 +68,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, content_type:, disposition:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = file_for(key).signed_url expires: expires_in, query: { "response-content-disposition" => content_disposition_with(type: disposition, filename: filename), "response-content-type" => content_type @@ -70,7 +81,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_type: content_type, content_md5: checksum @@ -85,8 +96,18 @@ module ActiveStorage end private + attr_reader :config + def file_for(key) bucket.file(key, skip_lookup: true) end + + def bucket + @bucket ||= client.bucket(config.fetch(:bucket)) + end + + def client + @client ||= Google::Cloud::Storage.new(config.except(:bucket)) + end end end diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb index 39e922f7ab..7eca8ce7f4 100644 --- a/activestorage/lib/active_storage/service/mirror_service.rb +++ b/activestorage/lib/active_storage/service/mirror_service.rb @@ -35,6 +35,11 @@ module ActiveStorage perform_across_services :delete, key end + # Delete files at keys starting with the +prefix+ on all services. + def delete_prefixed(prefix) + perform_across_services :delete_prefixed, prefix + end + private def each_service(&block) [ primary, *mirrors ].each(&block) diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index 6957119780..c95672f338 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -17,7 +17,7 @@ module ActiveStorage end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do begin object_for(key).put(upload_options.merge(body: io, content_md5: checksum)) rescue Aws::S3::Errors::BadDigest @@ -28,24 +28,30 @@ module ActiveStorage def download(key, &block) if block_given? - instrument :streaming_download, key do + instrument :streaming_download, key: key do stream(key, &block) end else - instrument :download, key do + instrument :download, key: key do object_for(key).get.body.read.force_encoding(Encoding::BINARY) end end end def delete(key) - instrument :delete, key do + instrument :delete, key: key do object_for(key).delete end end + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + bucket.objects(prefix: prefix).batch_delete! + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = object_for(key).exists? payload[:exist] = answer answer @@ -53,7 +59,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, disposition:, content_type:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i, response_content_disposition: content_disposition_with(type: disposition, filename: filename), response_content_type: content_type @@ -65,7 +71,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i, content_type: content_type, content_length: content_length, content_md5: checksum diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake index ef923e5926..296e91afa1 100644 --- a/activestorage/lib/tasks/activestorage.rake +++ b/activestorage/lib/tasks/activestorage.rake @@ -3,6 +3,10 @@ namespace :active_storage do desc "Copy over the migration needed to the application" task install: :environment do - Rake::Task["active_storage:install:migrations"].invoke + if Rake::Task.task_defined?("active_storage:install:migrations") + Rake::Task["active_storage:install:migrations"].invoke + else + Rake::Task["app:active_storage:install:migrations"].invoke + end end end diff --git a/activestorage/package.json b/activestorage/package.json index 8e6dd1c57f..d23f8b179e 100644 --- a/activestorage/package.json +++ b/activestorage/package.json @@ -1,10 +1,11 @@ { "name": "activestorage", - "version": "5.2.0-alpha", + "version": "6.0.0-alpha", "description": "Attach cloud and local files in Rails applications", "main": "app/assets/javascripts/activestorage.js", "files": [ - "app/assets/javascripts/*.js" + "app/assets/javascripts/*.js", + "src/*.js" ], "homepage": "http://rubyonrails.org/", "repository": { @@ -16,18 +17,21 @@ }, "author": "Javan Makhmali <javan@javan.us>", "license": "MIT", + "dependencies": { + "spark-md5": "^3.0.0" + }, "devDependencies": { "babel-core": "^6.25.0", "babel-loader": "^7.1.1", "babel-preset-env": "^1.6.0", "eslint": "^4.3.0", "eslint-plugin-import": "^2.7.0", - "spark-md5": "^3.0.0", "webpack": "^3.4.0" }, "scripts": { "prebuild": "yarn lint", "build": "webpack -p", - "lint": "eslint app/javascript" + "lint": "eslint app/javascript", + "prepublishOnly": "rm -rf src && cp -R app/javascript/activestorage src" } } diff --git a/activestorage/test/analyzer/image_analyzer_test.rb b/activestorage/test/analyzer/image_analyzer_test.rb index 9087072215..f8a38f001a 100644 --- a/activestorage/test/analyzer/image_analyzer_test.rb +++ b/activestorage/test/analyzer/image_analyzer_test.rb @@ -6,11 +6,32 @@ require "database/setup" require "active_storage/analyzer/image_analyzer" class ActiveStorage::Analyzer::ImageAnalyzerTest < ActiveSupport::TestCase - test "analyzing an image" do + test "analyzing a JPEG image" do blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") - metadata = blob.tap(&:analyze).metadata + metadata = extract_metadata_from(blob) assert_equal 4104, metadata[:width] assert_equal 2736, metadata[:height] end + + test "analyzing a rotated JPEG image" do + blob = create_file_blob(filename: "racecar_rotated.jpg", content_type: "image/jpeg") + metadata = extract_metadata_from(blob) + + assert_equal 2736, metadata[:width] + assert_equal 4104, metadata[:height] + end + + test "analyzing an SVG image without an XML declaration" do + blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml") + metadata = extract_metadata_from(blob) + + assert_equal 792, metadata[:width] + assert_equal 584, metadata[:height] + end + + private + def extract_metadata_from(blob) + blob.tap(&:analyze).metadata + end end diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb index 4a3c4a8bfc..2612006551 100644 --- a/activestorage/test/analyzer/video_analyzer_test.rb +++ b/activestorage/test/analyzer/video_analyzer_test.rb @@ -8,28 +8,52 @@ require "active_storage/analyzer/video_analyzer" class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase test "analyzing a video" do blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4") - metadata = blob.tap(&:analyze).metadata + metadata = extract_metadata_from(blob) assert_equal 640, metadata[:width] assert_equal 480, metadata[:height] - assert_equal [4, 3], metadata[:aspect_ratio] + assert_equal [4, 3], metadata[:display_aspect_ratio] assert_equal 5.166648, metadata[:duration] assert_not_includes metadata, :angle end test "analyzing a rotated video" do blob = create_file_blob(filename: "rotated_video.mp4", content_type: "video/mp4") - metadata = blob.tap(&:analyze).metadata + metadata = extract_metadata_from(blob) - assert_equal 640, metadata[:width] - assert_equal 480, metadata[:height] - assert_equal [4, 3], metadata[:aspect_ratio] + assert_equal 480, metadata[:width] + assert_equal 640, metadata[:height] + assert_equal [4, 3], metadata[:display_aspect_ratio] assert_equal 5.227975, metadata[:duration] assert_equal 90, metadata[:angle] end + test "analyzing a video with rectangular samples" do + blob = create_file_blob(filename: "video_with_rectangular_samples.mp4", content_type: "video/mp4") + metadata = extract_metadata_from(blob) + + assert_equal 1280, metadata[:width] + assert_equal 720, metadata[:height] + assert_equal [16, 9], metadata[:display_aspect_ratio] + end + + test "analyzing a video with an undefined display aspect ratio" do + blob = create_file_blob(filename: "video_with_undefined_display_aspect_ratio.mp4", content_type: "video/mp4") + metadata = extract_metadata_from(blob) + + assert_equal 640, metadata[:width] + assert_equal 480, metadata[:height] + assert_nil metadata[:display_aspect_ratio] + end + test "analyzing a video without a video stream" do blob = create_file_blob(filename: "video_without_video_stream.mp4", content_type: "video/mp4") - assert_equal({ "analyzed" => true }, blob.tap(&:analyze).metadata) + metadata = extract_metadata_from(blob) + assert_equal({ "analyzed" => true, "identified" => true }, metadata) end + + private + def extract_metadata_from(blob) + blob.tap(&:analyze).metadata + end end diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb index 97177e64c2..9c811df895 100644 --- a/activestorage/test/controllers/blobs_controller_test.rb +++ b/activestorage/test/controllers/blobs_controller_test.rb @@ -8,6 +8,11 @@ class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest @blob = create_file_blob filename: "racecar.jpg" end + test "showing blob with invalid signed ID" do + get rails_service_blob_url("invalid", "racecar.jpg") + assert_response :not_found + end + test "showing blob utilizes browser caching" do get rails_blob_url(@blob) diff --git a/activestorage/test/controllers/previews_controller_test.rb b/activestorage/test/controllers/previews_controller_test.rb index c3151a710e..b87be6c8b2 100644 --- a/activestorage/test/controllers/previews_controller_test.rb +++ b/activestorage/test/controllers/previews_controller_test.rb @@ -14,11 +14,20 @@ class ActiveStorage::PreviewsControllerTest < ActionDispatch::IntegrationTest signed_blob_id: @blob.signed_id, variation_key: ActiveStorage::Variation.encode(resize: "100x100")) - assert @blob.preview_image.attached? + assert_predicate @blob.preview_image, :attached? assert_redirected_to(/report\.png\?.*disposition=inline/) image = read_image(@blob.preview_image.variant(resize: "100x100")) assert_equal 77, image.width assert_equal 100, image.height end + + test "showing preview with invalid signed blob ID" do + get rails_blob_preview_url( + filename: @blob.filename, + signed_blob_id: "invalid", + variation_key: ActiveStorage::Variation.encode(resize: "100x100")) + + assert_response :not_found + end end diff --git a/activestorage/test/controllers/variants_controller_test.rb b/activestorage/test/controllers/variants_controller_test.rb index 6c70d73786..a0642f9bed 100644 --- a/activestorage/test/controllers/variants_controller_test.rb +++ b/activestorage/test/controllers/variants_controller_test.rb @@ -20,4 +20,13 @@ class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest assert_equal 100, image.width assert_equal 67, image.height end + + test "showing variant with invalid signed blob ID" do + get rails_blob_variation_url( + filename: @blob.filename, + signed_blob_id: "invalid", + variation_key: ActiveStorage::Variation.encode(resize: "100x100")) + + assert_response :not_found + end end diff --git a/activestorage/test/database/setup.rb b/activestorage/test/database/setup.rb index 705650a25d..daeeb5695b 100644 --- a/activestorage/test/database/setup.rb +++ b/activestorage/test/database/setup.rb @@ -3,5 +3,5 @@ require_relative "create_users_migration" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") -ActiveRecord::Migrator.migrate File.expand_path("../../db/migrate", __dir__) +ActiveRecord::Base.connection.migration_context.migrate ActiveStorageCreateUsers.migrate(:up) diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb index 7ee6625bb5..bd14ac0b1a 100644 --- a/activestorage/test/dummy/config/application.rb +++ b/activestorage/test/dummy/config/application.rb @@ -10,16 +10,12 @@ require "action_controller/railtie" require "action_view/railtie" require "sprockets/railtie" require "active_storage/engine" -#require "action_mailer/railtie" -#require "rails/test_unit/railtie" -#require "action_cable/engine" - Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application - config.load_defaults 5.1 + config.load_defaults 5.2 config.active_storage.service = :local end diff --git a/activestorage/test/dummy/config/environments/test.rb b/activestorage/test/dummy/config/environments/test.rb index ce0889e8ae..74a802d98c 100644 --- a/activestorage/test/dummy/config/environments/test.rb +++ b/activestorage/test/dummy/config/environments/test.rb @@ -30,6 +30,9 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end diff --git a/activestorage/test/fixtures/files/icon.psd b/activestorage/test/fixtures/files/icon.psd Binary files differnew file mode 100644 index 0000000000..631fceeaab --- /dev/null +++ b/activestorage/test/fixtures/files/icon.psd diff --git a/activestorage/test/fixtures/files/icon.svg b/activestorage/test/fixtures/files/icon.svg new file mode 100644 index 0000000000..6cfb0e241e --- /dev/null +++ b/activestorage/test/fixtures/files/icon.svg @@ -0,0 +1,13 @@ +<!-- The XML declaration is intentionally omitted. --> +<svg width="792px" height="584px" viewBox="0 0 792 584" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <path d="M9.51802657,28.724593 C9.51802657,18.2155955 18.1343454,9.60822622 28.6542694,9.60822622 L763.245541,9.60822622 C773.765465,9.60822622 782.381784,18.2155955 782.381784,28.724593 L782.381784,584 L792,584 L792,28.724593 C792,12.911054 779.075522,0 763.245541,0 L28.7544592,0 C12.9244782,0 0,12.911054 0,28.724593 L0,584 L9.61821632,584 C9.51802657,584 9.51802657,28.724593 9.51802657,28.724593 L9.51802657,28.724593 Z" id="Shape" opacity="0.3" fill="#CCCCCC"></path> + <circle id="Oval" fill="#FFCC33" cx="119.1" cy="147.2" r="33"></circle> + <circle id="Oval" fill="#3399FF" cx="119.1" cy="281.1" r="33"></circle> + <circle id="Oval" fill="#FF3333" cx="676.1" cy="376.8" r="33"></circle> + <circle id="Oval" fill="#FFCC33" cx="119.1" cy="477.2" r="33"></circle> + <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="130.5" width="442.1" height="33.5"></rect> + <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="183.1" width="442.1" height="33.5"></rect> + <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="265.8" width="442.1" height="33.5"></rect> + <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="363.4" width="442.1" height="33.5"></rect> + <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="465.3" width="442.1" height="33.5"></rect> +</svg> diff --git a/activestorage/test/fixtures/files/image.gif b/activestorage/test/fixtures/files/image.gif Binary files differnew file mode 100644 index 0000000000..90c05f671c --- /dev/null +++ b/activestorage/test/fixtures/files/image.gif diff --git a/activestorage/test/fixtures/files/racecar_rotated.jpg b/activestorage/test/fixtures/files/racecar_rotated.jpg Binary files differnew file mode 100644 index 0000000000..89e6d54f98 --- /dev/null +++ b/activestorage/test/fixtures/files/racecar_rotated.jpg diff --git a/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4 b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4 Binary files differnew file mode 100644 index 0000000000..12b04afc87 --- /dev/null +++ b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4 diff --git a/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4 b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4 Binary files differnew file mode 100644 index 0000000000..eb354e756f --- /dev/null +++ b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4 diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb index 47f2bd7911..9ba2759893 100644 --- a/activestorage/test/models/attachments_test.rb +++ b/activestorage/test/models/attachments_test.rb @@ -27,7 +27,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase test "attach new blob from an UploadedFile" do file = file_fixture "racecar.jpg" - @user.avatar.attach Rack::Test::UploadedFile.new file + @user.avatar.attach Rack::Test::UploadedFile.new file.to_s assert_equal "racecar.jpg", @user.avatar.filename.to_s end @@ -56,6 +56,40 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase assert ActiveStorage::Blob.service.exist?(@user.avatar.key) end + test "attach blob to new record" do + user = User.new(name: "Jason") + + assert_no_changes -> { user.new_record? } do + assert_no_difference -> { ActiveStorage::Attachment.count } do + user.avatar.attach create_blob(filename: "funky.jpg") + end + end + + assert_predicate user.avatar, :attached? + assert_equal "funky.jpg", user.avatar.filename.to_s + + assert_difference -> { ActiveStorage::Attachment.count }, +1 do + user.save! + end + + assert_predicate user.reload.avatar, :attached? + assert_equal "funky.jpg", user.avatar.filename.to_s + end + + test "build new record with attached blob" do + assert_no_difference -> { ActiveStorage::Attachment.count } do + @user = User.new(name: "Jason", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }) + end + + assert_predicate @user, :new_record? + assert_predicate @user.avatar, :attached? + assert_equal "town.jpg", @user.avatar.filename.to_s + + @user.save! + assert_predicate @user.reload.avatar, :attached? + assert_equal "town.jpg", @user.avatar.filename.to_s + end + test "access underlying associations of new blob" do @user.avatar.attach create_blob(filename: "funky.jpg") assert_equal @user, @user.avatar_attachment.record @@ -63,6 +97,29 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase assert_equal "funky.jpg", @user.avatar_attachment.blob.filename.to_s end + test "identify newly-attached, directly-uploaded blob" do + # Simulate a direct upload. + blob = create_blob_before_direct_upload(filename: "racecar.jpg", content_type: "application/octet-stream", byte_size: 1124062, checksum: "7GjDDNEQb4mzMzsW+MS0JQ==") + ActiveStorage::Blob.service.upload(blob.key, file_fixture("racecar.jpg").open) + + stub_request(:get, %r{localhost:3000/rails/active_storage/disk/.*}).to_return(body: file_fixture("racecar.jpg")) + @user.avatar.attach(blob) + + assert_equal "image/jpeg", @user.avatar.reload.content_type + assert_predicate @user.avatar, :identified? + end + + test "identify newly-attached blob only once" do + blob = create_file_blob + assert_predicate blob, :identified? + + # The blob's backing file is a PNG image. Fudge its content type so we can tell if it's identified when we attach it. + blob.update! content_type: "application/octet-stream" + + @user.avatar.attach blob + assert_equal "application/octet-stream", blob.content_type + end + test "analyze newly-attached blob" do perform_enqueued_jobs do @user.avatar.attach create_file_blob @@ -79,21 +136,42 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase @user.avatar.attach blob end - assert blob.reload.analyzed? + assert_predicate blob.reload, :analyzed? - @user.avatar.attachment.destroy + @user.avatar.detach assert_no_enqueued_jobs do @user.reload.avatar.attach blob end end + test "preserve existing metadata when analyzing a newly-attached blob" do + blob = create_file_blob(metadata: { foo: "bar" }) + + perform_enqueued_jobs do + @user.avatar.attach blob + end + + assert_equal "bar", blob.reload.metadata[:foo] + end + + test "detach blob" do + @user.avatar.attach create_blob(filename: "funky.jpg") + avatar_blob_id = @user.avatar.blob.id + avatar_key = @user.avatar.key + + @user.avatar.detach + assert_not_predicate @user.avatar, :attached? + assert ActiveStorage::Blob.exists?(avatar_blob_id) + assert ActiveStorage::Blob.service.exist?(avatar_key) + end + test "purge attached blob" do @user.avatar.attach create_blob(filename: "funky.jpg") avatar_key = @user.avatar.key @user.avatar.purge - assert_not @user.avatar.attached? + assert_not_predicate @user.avatar, :attached? assert_not ActiveStorage::Blob.service.exist?(avatar_key) end @@ -139,6 +217,48 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase assert_equal "country.jpg", @user.highlights.second.filename.to_s end + test "attach blobs to new record" do + user = User.new(name: "Jason") + + assert_no_changes -> { user.new_record? } do + assert_no_difference -> { ActiveStorage::Attachment.count } do + user.highlights.attach( + { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, + { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }) + end + end + + assert_predicate user.highlights, :attached? + assert_equal "town.jpg", user.highlights.first.filename.to_s + assert_equal "country.jpg", user.highlights.second.filename.to_s + + assert_difference -> { ActiveStorage::Attachment.count }, +2 do + user.save! + end + + assert_predicate user.reload.highlights, :attached? + assert_equal "town.jpg", user.highlights.first.filename.to_s + assert_equal "country.jpg", user.highlights.second.filename.to_s + end + + test "build new record with attached blobs" do + assert_no_difference -> { ActiveStorage::Attachment.count } do + @user = User.new(name: "Jason", highlights: [ + { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, + { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }]) + end + + assert_predicate @user, :new_record? + assert_predicate @user.highlights, :attached? + assert_equal "town.jpg", @user.highlights.first.filename.to_s + assert_equal "country.jpg", @user.highlights.second.filename.to_s + + @user.save! + assert_predicate @user.reload.highlights, :attached? + assert_equal "town.jpg", @user.highlights.first.filename.to_s + assert_equal "country.jpg", @user.highlights.second.filename.to_s + end + test "find attached blobs" do @user.highlights.attach( { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, @@ -193,12 +313,42 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase end end + test "preserve existing metadata when analyzing newly-attached blobs" do + blobs = [ + create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: { foo: "bar" }), + create_file_blob(filename: "video.mp4", content_type: "video/mp4", metadata: { foo: "bar" }) + ] + + perform_enqueued_jobs do + @user.highlights.attach(blobs) + end + + blobs.each do |blob| + assert_equal "bar", blob.reload.metadata[:foo] + end + end + + test "detach blobs" do + @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") + highlight_blob_ids = @user.highlights.collect { |highlight| highlight.blob.id } + highlight_keys = @user.highlights.collect(&:key) + + @user.highlights.detach + assert_not_predicate @user.highlights, :attached? + + assert ActiveStorage::Blob.exists?(highlight_blob_ids.first) + assert ActiveStorage::Blob.exists?(highlight_blob_ids.second) + + assert ActiveStorage::Blob.service.exist?(highlight_keys.first) + assert ActiveStorage::Blob.service.exist?(highlight_keys.second) + end + test "purge attached blobs" do @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") highlight_keys = @user.highlights.collect(&:key) @user.highlights.purge - assert_not @user.highlights.attached? + assert_not_predicate @user.highlights, :attached? assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first) assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second) end diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 6e815997ba..9b555f9a1d 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -2,8 +2,11 @@ require "test_helper" require "database/setup" +require "active_support/testing/method_call_assertions" class ActiveStorage::BlobTest < ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + test "create after upload sets byte size and checksum" do data = "Hello world!" blob = create_blob data: data @@ -13,10 +16,32 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase assert_equal Digest::MD5.base64digest(data), blob.checksum end + test "create after upload extracts content type from data" do + blob = create_file_blob content_type: "application/octet-stream" + assert_equal "image/jpeg", blob.content_type + end + + test "create after upload extracts content type from filename" do + blob = create_blob content_type: "application/octet-stream" + assert_equal "text/plain", blob.content_type + end + + test "image?" do + blob = create_file_blob filename: "racecar.jpg" + assert_predicate blob, :image? + assert_not_predicate blob, :audio? + end + + test "video?" do + blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4") + assert_predicate blob, :video? + assert_not_predicate blob, :audio? + end + test "text?" do blob = create_blob data: "Hello world!" - assert blob.text? - assert_not blob.audio? + assert_predicate blob, :text? + assert_not_predicate blob, :audio? end test "download yields chunks" do @@ -41,16 +66,63 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase end end - test "purge removes from external service" do + test "urls force attachment as content disposition for content types served as binary" do + blob = create_blob(content_type: "text/html") + + freeze_time do + assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url + assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url(disposition: :inline) + end + end + + test "urls allow for custom filename" do + blob = create_blob(filename: "original.txt") + new_filename = ActiveStorage::Filename.new("new.txt") + + freeze_time do + assert_equal expected_url_for(blob), blob.service_url + assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: new_filename) + assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: "new.txt") + assert_equal expected_url_for(blob, filename: blob.filename), blob.service_url(filename: nil) + end + end + + test "urls allow for custom options" do + blob = create_blob(filename: "original.txt") + + options = [ + blob.key, + expires_in: blob.service.url_expires_in, + disposition: :inline, + content_type: blob.content_type, + filename: blob.filename, + thumb_size: "300x300", + thumb_mode: "crop" + ] + assert_called_with(blob.service, :url, options) do + blob.service_url(thumb_size: "300x300", thumb_mode: "crop") + end + end + + test "purge deletes file from external service" do blob = create_blob blob.purge assert_not ActiveStorage::Blob.service.exist?(blob.key) end + test "purge deletes variants from external service" do + blob = create_file_blob + variant = blob.variant(resize: "100>").processed + + blob.purge + assert_not ActiveStorage::Blob.service.exist?(variant.key) + end + private - def expected_url_for(blob, disposition: :inline) - query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{blob.filename.parameters}" }.to_param - "/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{blob.filename}?#{query_string}" + 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 + "http://localhost:3000/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/preview_test.rb b/activestorage/test/models/preview_test.rb index bcd8442f4b..55fdc228c8 100644 --- a/activestorage/test/models/preview_test.rb +++ b/activestorage/test/models/preview_test.rb @@ -8,7 +8,7 @@ class ActiveStorage::PreviewTest < ActiveSupport::TestCase blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf") preview = blob.preview(resize: "640x280").processed - assert preview.image.attached? + assert_predicate preview.image, :attached? assert_equal "report.png", preview.image.filename.to_s assert_equal "image/png", preview.image.content_type @@ -21,7 +21,7 @@ class ActiveStorage::PreviewTest < ActiveSupport::TestCase blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4") preview = blob.preview(resize: "640x280").processed - assert preview.image.attached? + assert_predicate preview.image, :attached? assert_equal "video.png", preview.image.filename.to_s assert_equal "image/png", preview.image.content_type @@ -33,7 +33,7 @@ class ActiveStorage::PreviewTest < ActiveSupport::TestCase test "previewing an unpreviewable blob" do blob = create_file_blob - assert_raises ActiveStorage::Blob::UnpreviewableError do + assert_raises ActiveStorage::UnpreviewableError do blob.preview resize: "640x280" end end diff --git a/activestorage/test/models/representation_test.rb b/activestorage/test/models/representation_test.rb index 29fe61aee4..2a06b31c77 100644 --- a/activestorage/test/models/representation_test.rb +++ b/activestorage/test/models/representation_test.rb @@ -34,7 +34,7 @@ class ActiveStorage::RepresentationTest < ActiveSupport::TestCase test "representing an unrepresentable blob" do blob = create_blob - assert_raises ActiveStorage::Blob::UnrepresentableError do + assert_raises ActiveStorage::UnrepresentableError do blob.representation resize: "100x100" end end diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index b7d20ab55a..0cf8a583bd 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -4,12 +4,9 @@ require "test_helper" require "database/setup" class ActiveStorage::VariantTest < ActiveSupport::TestCase - setup do - @blob = create_file_blob filename: "racecar.jpg" - end - - test "resized variation" do - variant = @blob.variant(resize: "100x100").processed + test "resized variation of JPEG blob" do + blob = create_file_blob(filename: "racecar.jpg") + variant = blob.variant(resize: "100x100").processed assert_match(/racecar\.jpg/, variant.service_url) image = read_image(variant) @@ -17,8 +14,9 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_equal 67, image.height end - test "resized and monochrome variation" do - variant = @blob.variant(resize: "100x100", monochrome: true).processed + test "resized and monochrome variation of JPEG blob" do + blob = create_file_blob(filename: "racecar.jpg") + variant = blob.variant(resize: "100x100", monochrome: true).processed assert_match(/racecar\.jpg/, variant.service_url) image = read_image(variant) @@ -27,8 +25,48 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_match(/Gray/, image.colorspace) end + test "center-weighted crop of JPEG blob" do + blob = create_file_blob(filename: "racecar.jpg") + variant = blob.variant(combine_options: { + gravity: "center", + resize: "100x100^", + crop: "100x100+0+0", + }).processed + assert_match(/racecar\.jpg/, variant.service_url) + + image = read_image(variant) + assert_equal 100, image.width + assert_equal 100, image.height + end + + test "resized variation of PSD blob" do + blob = create_file_blob(filename: "icon.psd", content_type: "image/vnd.adobe.photoshop") + variant = blob.variant(resize: "20x20").processed + assert_match(/icon\.png/, variant.service_url) + + image = read_image(variant) + assert_equal "PNG", image.type + assert_equal 20, image.width + assert_equal 20, image.height + end + + test "optimized variation of GIF blob" do + blob = create_file_blob(filename: "image.gif", content_type: "image/gif") + + assert_nothing_raised do + blob.variant(layers: "Optimize").processed + end + end + + test "variation of invariable blob" do + assert_raises ActiveStorage::InvariableError do + create_file_blob(filename: "report.pdf", content_type: "application/pdf").variant(resize: "100x100") + end + end + test "service_url doesn't grow in length despite long variant options" do - variant = @blob.variant(font: "a" * 10_000).processed - assert_operator variant.service_url.length, :<, 500 + blob = create_file_blob(filename: "racecar.jpg") + variant = blob.variant(font: "a" * 10_000).processed + assert_operator variant.service_url.length, :<, 525 end end diff --git a/activestorage/test/service/configurations.example.yml b/activestorage/test/service/configurations.example.yml index 56ed37be5d..43cc013bc8 100644 --- a/activestorage/test/service/configurations.example.yml +++ b/activestorage/test/service/configurations.example.yml @@ -7,7 +7,7 @@ # # gcs: # service: GCS -# keyfile: { +# credentials: { # type: "service_account", # project_id: "", # private_key_id: "", diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb index a2fd035e02..fe8a637ad0 100644 --- a/activestorage/test/service/configurator_test.rb +++ b/activestorage/test/service/configurator_test.rb @@ -5,7 +5,10 @@ require "service/shared_service_tests" class ActiveStorage::Service::ConfiguratorTest < ActiveSupport::TestCase test "builds correct service instance based on service name" do service = ActiveStorage::Service::Configurator.build(:foo, foo: { service: "Disk", root: "path" }) + assert_instance_of ActiveStorage::Service::DiskService, service + assert_equal "path", service.root + assert_equal "http://localhost:3000", service.host end test "raises error when passing non-existent service name" do diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb index 5566c664a9..7efcd60fb7 100644 --- a/activestorage/test/service/gcs_service_test.rb +++ b/activestorage/test/service/gcs_service_test.rb @@ -32,12 +32,21 @@ if SERVICE_CONFIGURATIONS[:gcs] end test "signed URL generation" do - freeze_time do - url = SERVICE.bucket.signed_url(FIXTURE_KEY, expires: 120) + - "&response-content-disposition=inline%3B+filename%3D%22test.txt%22%3B+filename%2A%3DUTF-8%27%27test.txt" + - "&response-content-type=text%2Fplain" + assert_match(/storage\.googleapis\.com\/.*response-content-disposition=inline.*test\.txt.*response-content-type=text%2Fplain/, + @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")) + end + + test "signed URL response headers" do + begin + key = SecureRandom.base58(24) + data = "Something else entirely!" + @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data)) - assert_equal url, @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain") + url = @service.url(key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain") + response = Net::HTTP.get_response(URI(url)) + assert_equal "text/plain", response.header["Content-Type"] + ensure + @service.delete key end end end diff --git a/activestorage/test/service/mirror_service_test.rb b/activestorage/test/service/mirror_service_test.rb index 92101b1282..87306644c5 100644 --- a/activestorage/test/service/mirror_service_test.rb +++ b/activestorage/test/service/mirror_service_test.rb @@ -10,8 +10,8 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase end.to_h config = mirror_config.merge \ - mirror: { service: "Mirror", primary: "primary", mirrors: mirror_config.keys }, - primary: { service: "Disk", root: Dir.mktmpdir("active_storage_tests_primary") } + mirror: { service: "Mirror", primary: "primary", mirrors: mirror_config.keys }, + primary: { service: "Disk", root: Dir.mktmpdir("active_storage_tests_primary") } SERVICE = ActiveStorage::Service.configure :mirror, config @@ -19,11 +19,16 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase test "uploading to all services" do begin - data = "Something else entirely!" - key = upload(data, to: @service) + key = SecureRandom.base58(24) + data = "Something else entirely!" + io = StringIO.new(data) + checksum = Digest::MD5.base64digest(data) - assert_equal data, SERVICE.primary.download(key) - SERVICE.mirrors.each do |mirror| + @service.upload key, io.tap(&:read), checksum: checksum + assert_predicate io, :eof? + + assert_equal data, @service.primary.download(key) + @service.mirrors.each do |mirror| assert_equal data, mirror.download(key) end ensure @@ -32,14 +37,18 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase end test "downloading from primary service" do - data = "Something else entirely!" - key = upload(data, to: SERVICE.primary) + key = SecureRandom.base58(24) + data = "Something else entirely!" + checksum = Digest::MD5.base64digest(data) + + @service.primary.upload key, StringIO.new(data), checksum: checksum assert_equal data, @service.download(key) end test "deleting from all services" do @service.delete FIXTURE_KEY + assert_not SERVICE.primary.exist?(FIXTURE_KEY) SERVICE.mirrors.each do |mirror| assert_not mirror.exist?(FIXTURE_KEY) @@ -50,17 +59,8 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase filename = ActiveStorage::Filename.new("test.txt") freeze_time do - assert_equal SERVICE.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain"), + assert_equal @service.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain"), @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain") end end - - private - def upload(data, to:) - SecureRandom.base58(24).tap do |key| - io = StringIO.new(data).tap(&:read) - @service.upload key, io, checksum: Digest::MD5.base64digest(data) - assert io.eof? - end - end end diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb index ade91ab89a..ce28c4393a 100644 --- a/activestorage/test/service/shared_service_tests.rb +++ b/activestorage/test/service/shared_service_tests.rb @@ -75,5 +75,22 @@ module ActiveStorage::Service::SharedServiceTests @service.delete SecureRandom.base58(24) end end + + test "deleting by prefix" do + begin + @service.upload("a/a/a", StringIO.new(FIXTURE_DATA)) + @service.upload("a/a/b", StringIO.new(FIXTURE_DATA)) + @service.upload("a/b/a", StringIO.new(FIXTURE_DATA)) + + @service.delete_prefixed("a/a/") + assert_not @service.exist?("a/a/a") + assert_not @service.exist?("a/a/b") + assert @service.exist?("a/b/a") + ensure + @service.delete("a/a/a") + @service.delete("a/a/b") + @service.delete("a/b/a") + end + end end end diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb index 80f183e0e3..f0b166c225 100644 --- a/activestorage/test/template/image_tag_test.rb +++ b/activestorage/test/template/image_tag_test.rb @@ -33,7 +33,7 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase test "error when attachment's empty" do @user = User.create!(name: "DHH") - assert_not @user.avatar.attached? + assert_not_predicate @user.avatar, :attached? assert_raises(ArgumentError) { image_tag(@user.avatar) } end diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 38408cdad3..98fa44a604 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true +ENV["RAILS_ENV"] ||= "test" require_relative "dummy/config/environment.rb" require "bundler/setup" require "active_support" require "active_support/test_case" require "active_support/testing/autorun" +require "webmock/minitest" require "mini_magick" begin @@ -40,13 +42,15 @@ ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing") class ActiveSupport::TestCase self.file_fixture_path = File.expand_path("fixtures/files", __dir__) + setup { WebMock.allow_net_connect! } + private def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain") ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type end - def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") - ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type + def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: nil) + ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata end def create_blob_before_direct_upload(filename: "hello.txt", byte_size:, checksum:, content_type: "text/plain") diff --git a/activestorage/webpack.config.js b/activestorage/webpack.config.js index 92c4530e7f..3a50eef470 100644 --- a/activestorage/webpack.config.js +++ b/activestorage/webpack.config.js @@ -1,4 +1,3 @@ -const webpack = require("webpack") const path = require("path") module.exports = { diff --git a/activestorage/yarn.lock b/activestorage/yarn.lock index dd09577445..41742be201 100644 --- a/activestorage/yarn.lock +++ b/activestorage/yarn.lock @@ -1219,12 +1219,6 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-config-airbnb-base@^11.3.1: - version "11.3.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.3.1.tgz#c0ab108c9beed503cb999e4c60f4ef98eda0ed30" - dependencies: - eslint-restricted-globals "^0.1.1" - eslint-import-resolver-node@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc" @@ -1254,10 +1248,6 @@ eslint-plugin-import@^2.7.0: minimatch "^3.0.3" read-pkg-up "^2.0.0" -eslint-restricted-globals@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" - eslint-scope@^3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 889919855c..c9cf63f7b5 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,365 +1,3 @@ -* Allow `Range#include?` on TWZ ranges - In #11474 we prevented TWZ ranges being iterated over which matched - Ruby's handling of Time ranges and as a consequence `include?` - stopped working with both Time ranges and TWZ ranges. However in - ruby/ruby@b061634 support was added for `include?` to use `cover?` - for 'linear' objects. Since we have no way of making Ruby consider - TWZ instances as 'linear' we have to override `Range#include?`. - Fixes #30799. - - *Andrew White* - -* Fix acronym support in `humanize` - - Acronym inflections are stored with lowercase keys in the hash but - the match wasn't being lowercased before being looked up in the hash. - This shouldn't have any performance impact because before it would - fail to find the acronym and perform the `downcase` operation anyway. - - Fixes #31052. - - *Andrew White* - -* Add same method signature for `Time#prev_year` and `Time#next_year` - in accordance with `Date#prev_year`, `Date#next_year`. - - Allows pass argument for `Time#prev_year` and `Time#next_year`. - - Before: - ``` - Time.new(2017, 9, 16, 17, 0).prev_year # => 2016-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_year(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - - Time.new(2017, 9, 16, 17, 0).next_year # => 2018-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_year(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - ``` - - After: - ``` - Time.new(2017, 9, 16, 17, 0).prev_year # => 2016-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_year(1) # => 2016-09-16 17:00:00 +0300 - - Time.new(2017, 9, 16, 17, 0).next_year # => 2018-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_year(1) # => 2018-09-16 17:00:00 +0300 - ``` - - *bogdanvlviv* - -* Add same method signature for `Time#prev_month` and `Time#next_month` - in accordance with `Date#prev_month`, `Date#next_month`. - - Allows pass argument for `Time#prev_month` and `Time#next_month`. - - Before: - ``` - Time.new(2017, 9, 16, 17, 0).prev_month # => 2017-08-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_month(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - - Time.new(2017, 9, 16, 17, 0).next_month # => 2017-10-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_month(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - ``` - - After: - ``` - Time.new(2017, 9, 16, 17, 0).prev_month # => 2017-08-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_month(1) # => 2017-08-16 17:00:00 +0300 - - Time.new(2017, 9, 16, 17, 0).next_month # => 2017-10-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_month(1) # => 2017-10-16 17:00:00 +0300 - ``` - - *bogdanvlviv* - -* Add same method signature for `Time#prev_day` and `Time#next_day` - in accordance with `Date#prev_day`, `Date#next_day`. - - Allows pass argument for `Time#prev_day` and `Time#next_day`. - - Before: - ``` - Time.new(2017, 9, 16, 17, 0).prev_day # => 2017-09-15 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_day(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - - Time.new(2017, 9, 16, 17, 0).next_day # => 2017-09-17 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_day(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - ``` - - After: - ``` - Time.new(2017, 9, 16, 17, 0).prev_day # => 2017-09-15 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_day(1) # => 2017-09-15 17:00:00 +0300 - - Time.new(2017, 9, 16, 17, 0).next_day # => 2017-09-17 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_day(1) # => 2017-09-17 17:00:00 +0300 - ``` - - *bogdanvlviv* - -* `IO#to_json` now returns the `to_s` representation, rather than - attempting to convert to an array. This fixes a bug where `IO#to_json` - would raise an `IOError` when called on an unreadable object. - - Fixes #26132. - - *Paul Kuruvilla* - -* Remove deprecated `halt_callback_chains_on_return_false` option. - - *Rafael Mendonça França* - -* Remove deprecated `:if` and `:unless` string filter for callbacks. - - *Rafael Mendonça França* - -* `Hash#slice` now falls back to Ruby 2.5+'s built-in definition if defined. - - *Akira Matsuda* - -* Deprecate `secrets.secret_token`. - - The architecture for secrets had a big upgrade between Rails 3 and Rails 4, - when the default changed from using `secret_token` to `secret_key_base`. - - `secret_token` has been soft deprecated in documentation for four years - but is still in place to support apps created before Rails 4. - Deprecation warnings have been added to help developers upgrade their - applications to `secret_key_base`. - - *claudiob*, *Kasper Timm Hansen* - -* Return an instance of `HashWithIndifferentAccess` from `HashWithIndifferentAccess#transform_keys`. - - *Yuji Yaginuma* - -* Add key rotation support to `MessageEncryptor` and `MessageVerifier` - - This change introduces a `rotate` method to both the `MessageEncryptor` and - `MessageVerifier` classes. This method accepts the same arguments and - options as the given classes' constructor. The `encrypt_and_verify` method - for `MessageEncryptor` and the `verified` method for `MessageVerifier` also - accept an optional keyword argument `:on_rotation` block which is called - when a rotated instance is used to decrypt or verify the message. - - *Michael J Coyne* - -* Deprecate `Module#reachable?` method. - - *bogdanvlviv* - -* Add `config/credentials.yml.enc` to store production app secrets. - - Allows saving any authentication credentials for third party services - directly in repo encrypted with `config/master.key` or `ENV["RAILS_MASTER_KEY"]`. - - This will eventually replace `Rails.application.secrets` and the encrypted - secrets introduced in Rails 5.1. - - *DHH*, *Kasper Timm Hansen* - -* Add `ActiveSupport::EncryptedFile` and `ActiveSupport::EncryptedConfiguration`. - - Allows for stashing encrypted files or configuration directly in repo by - encrypting it with a key. - - Backs the new credentials setup above, but can also be used independently. - - *DHH*, *Kasper Timm Hansen* - -* `Module#delegate_missing_to` now raises `DelegationError` if target is nil, - similar to `Module#delegate`. - - *Anton Khamets* - -* Update `String#camelize` to provide feedback when wrong option is passed - - `String#camelize` was returning nil without any feedback when an - invalid option was passed as a parameter. - - Previously: - - 'one_two'.camelize(true) - # => nil - - Now: - - 'one_two'.camelize(true) - # => ArgumentError: Invalid option, use either :upper or :lower. - - *Ricardo Díaz* - -* Fix modulo operations involving durations - - Rails 5.1 introduced `ActiveSupport::Duration::Scalar` as a wrapper - around numeric values as a way of ensuring a duration was the outcome of - an expression. However, the implementation was missing support for modulo - operations. This support has now been added and should result in a duration - being returned from expressions involving modulo operations. - - Prior to Rails 5.1: - - 5.minutes % 2.minutes - # => 60 - - Now: - - 5.minutes % 2.minutes - # => 1 minute - - Fixes #29603 and #29743. - - *Sayan Chakraborty*, *Andrew White* - -* Fix division where a duration is the denominator - - PR #29163 introduced a change in behavior when a duration was the denominator - in a calculation - this was incorrect as dividing by a duration should always - return a `Numeric`. The behavior of previous versions of Rails has been restored. - - Fixes #29592. - - *Andrew White* - -* Add purpose and expiry support to `ActiveSupport::MessageVerifier` & - `ActiveSupport::MessageEncryptor`. - - For instance, to ensure a message is only usable for one intended purpose: - - token = @verifier.generate("x", purpose: :shipping) - - @verifier.verified(token, purpose: :shipping) # => "x" - @verifier.verified(token) # => nil - - Or make it expire after a set time: - - @verifier.generate("x", expires_in: 1.month) - @verifier.generate("y", expires_at: Time.now.end_of_year) - - Showcased with `ActiveSupport::MessageVerifier`, but works the same for - `ActiveSupport::MessageEncryptor`'s `encrypt_and_sign` and `decrypt_and_verify`. - - Pull requests: #29599, #29854 - - *Assain Jaleel* - -* Make the order of `Hash#reverse_merge!` consistent with `HashWithIndifferentAccess`. - - *Erol Fornoles* - -* Add `freeze_time` helper which freezes time to `Time.now` in tests. - - *Prathamesh Sonpatki* - -* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption. - - On for new Rails 5.2 apps. Upgrading apps can find the config as a new - framework default. - - *Assain Jaleel* - -* Cache: `write_multi` - - Rails.cache.write_multi foo: 'bar', baz: 'qux' - - Plus faster fetch_multi with stores that implement `write_multi_entries`. - Keys that aren't found may be written to the cache store in one shot - instead of separate writes. - - The default implementation simply calls `write_entry` for each entry. - Stores may override if they're capable of one-shot bulk writes, like - Redis `MSET`. - - *Jeremy Daer* - -* Add default option to module and class attribute accessors. - - mattr_accessor :settings, default: {} - - Works for `mattr_reader`, `mattr_writer`, `cattr_accessor`, `cattr_reader`, - and `cattr_writer` as well. - - *Genadi Samokovarov* - -* Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week. - - *Shota Iguchi* - -* Add default option to `class_attribute`. - - Before: - - class_attribute :settings - self.settings = {} - - Now: - - class_attribute :settings, default: {} - - *DHH* - -* `#singularize` and `#pluralize` now respect uncountables for the specified locale. - - *Eilis Hamilton* - -* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton. - Primary use case is keeping all the per-request attributes easily available to the whole system. - - *DHH* - -* Fix implicit coercion calculations with scalars and durations - - Previously, calculations where the scalar is first would be converted to a duration - of seconds, but this causes issues with dates being converted to times, e.g: - - Time.zone = "Beijing" # => Asia/Shanghai - date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 - 2 * 1.day # => 172800 seconds - date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 - - Now, the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain - the part structure of the duration where possible, e.g: - - Time.zone = "Beijing" # => Asia/Shanghai - date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 - 2 * 1.day # => 2 days - date + 2 * 1.day # => Mon, 22 May 2017 - - Fixes #29160, #28970. - - *Andrew White* - -* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving - on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version` - in Active Record and its use in Action Pack's fragment caching. - - *DHH* - -* Pass gem name and deprecation horizon to deprecation notifications. - - *Willem van Bergen* - -* Add support for `:offset` and `:zone` to `ActiveSupport::TimeWithZone#change` - - *Andrew White* - -* Add support for `:offset` to `Time#change` - - Fixes #28723. - - *Andrew White* - -* Add `fetch_values` for `HashWithIndifferentAccess` - - The method was originally added to `Hash` in Ruby 2.3.0. - - *Josh Pencheon* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 6b3cead1a7..8f769c0767 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2017 David Heinemeier Hansson +Copyright (c) 2005-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index 8b47933bd2..c770324be8 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -30,7 +30,7 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 8672ab1542..f10f19be0a 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -14,10 +14,30 @@ Rake::TestTask.new do |t| t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end +Rake::Task[:test].enhance do + Rake::Task["test:cache_stores:redis:ruby"].invoke +end + namespace :test do task :isolated do Dir.glob("test/**/*_test.rb").all? do |file| sh(Gem.ruby, "-w", "-Ilib:test", file) end || raise("Failures") end + + namespace :cache_stores do + namespace :redis do + %w[ ruby hiredis ].each do |driver| + task("env:#{driver}") { ENV["REDIS_DRIVER"] = driver } + + Rake::TestTask.new(driver => "env:#{driver}") do |t| + t.libs << "test" + t.test_files = ["test/cache/stores/redis_cache_store_test.rb"] + t.warning = true + t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) + end + end + end + end end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 68be94f99d..a4fb697669 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2005-2017 David Heinemeier Hansson +# Copyright (c) 2005-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -53,6 +53,7 @@ module ActiveSupport autoload :Callbacks autoload :Configurable autoload :Deprecation + autoload :Digest autoload :Gzip autoload :Inflector autoload :JSON diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 9f91f9b576..2d038dba77 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -12,10 +12,11 @@ require "active_support/core_ext/string/inflections" module ActiveSupport # See ActiveSupport::Cache::Store for documentation. module Cache - autoload :FileStore, "active_support/cache/file_store" - autoload :MemoryStore, "active_support/cache/memory_store" - autoload :MemCacheStore, "active_support/cache/mem_cache_store" - autoload :NullStore, "active_support/cache/null_store" + autoload :FileStore, "active_support/cache/file_store" + autoload :MemoryStore, "active_support/cache/memory_store" + autoload :MemCacheStore, "active_support/cache/mem_cache_store" + autoload :NullStore, "active_support/cache/null_store" + autoload :RedisCacheStore, "active_support/cache/redis_cache_store" # These options mean something to all cache implementations. Individual cache # implementations may support additional options. @@ -90,16 +91,11 @@ module ActiveSupport private def retrieve_cache_key(key) case - when key.respond_to?(:cache_key_with_version) - key.cache_key_with_version - when key.respond_to?(:cache_key) - key.cache_key - when key.is_a?(Hash) - key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" }.to_param - when key.respond_to?(:to_a) - key.to_a.collect { |element| retrieve_cache_key(element) }.to_param - else - key.to_param + when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version + when key.respond_to?(:cache_key) then key.cache_key + when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) + else key.to_param end.to_s end @@ -153,12 +149,11 @@ module ActiveSupport # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace # - # Caches can also store values in a compressed format to save space and - # reduce time spent sending data. Since there is overhead, values must be - # large enough to warrant compression. To turn on compression either pass - # <tt>compress: true</tt> in the initializer or as an option to +fetch+ - # or +write+. To specify the threshold at which to compress values, set the - # <tt>:compress_threshold</tt> option. The default threshold is 16K. + # Cached data larger than 1kB are compressed by default. To turn off + # compression, pass <tt>compress: false</tt> to the initializer or to + # individual +fetch+ or +write+ method calls. The 1kB compression + # threshold is configurable with the <tt>:compress_threshold</tt> option, + # specified in bytes. class Store cattr_accessor :logger, instance_writer: true @@ -217,8 +212,7 @@ module ActiveSupport # ask whether you should force a cache write. Otherwise, it's clearer to # just call <tt>Cache#write</tt>. # - # Setting <tt>:compress</tt> will store a large cache entry set by the call - # in a compressed format. + # Setting <tt>compress: false</tt> disables compression of the cache entry. # # Setting <tt>:expires_in</tt> will set an expiration time on the cache. # All caches support auto-expiring content after a specified number of @@ -363,23 +357,11 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - results = {} - names.each do |name| - key = normalize_key(name, options) - version = normalize_version(name, options) - entry = read_entry(key, options) - - if entry - if entry.expired? - delete_entry(key, options) - elsif entry.mismatched?(version) - # Skip mismatched versions - else - results[name] = entry.value - end + instrument :read_multi, names, options do |payload| + read_multi_entries(names, options).tap do |results| + payload[:hits] = results.keys end end - results end # Cache Storage API to write multiple values at once. @@ -420,14 +402,19 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - read_multi(*names, options).tap do |results| - writes = {} + instrument :read_multi, names, options do |payload| + read_multi_entries(names, options).tap do |results| + payload[:hits] = results.keys + payload[:super_operation] = :fetch_multi - (names - results.keys).each do |name| - results[name] = writes[name] = yield(name) - end + writes = {} + + (names - results.keys).each do |name| + results[name] = writes[name] = yield(name) + end - write_multi writes, options + write_multi writes, options + end end end @@ -544,6 +531,28 @@ module ActiveSupport raise NotImplementedError.new end + # Reads multiple entries from the cache implementation. Subclasses MAY + # implement this method. + def read_multi_entries(names, options) + results = {} + names.each do |name| + key = normalize_key(name, options) + version = normalize_version(name, options) + entry = read_entry(key, options) + + if entry + if entry.expired? + delete_entry(key, options) + elsif entry.mismatched?(version) + # Skip mismatched versions + else + results[name] = entry.value + end + end + end + results + end + # Writes multiple entries to the cache implementation. Subclasses MAY # implement this method. def write_multi_entries(hash, options) @@ -567,14 +576,54 @@ module ActiveSupport end end - # Prefixes a key with the namespace. Namespace and key will be delimited - # with a colon. - def normalize_key(key, options) - key = Cache.expand_cache_key(key) - namespace = options[:namespace] if options - prefix = namespace.is_a?(Proc) ? namespace.call : namespace - key = "#{prefix}:#{key}" if prefix - key + # Expands and namespaces the cache key. May be overridden by + # cache stores to do additional normalization. + def normalize_key(key, options = nil) + namespace_key expanded_key(key), options + end + + # Prefix the key with a namespace string: + # + # namespace_key 'foo', namespace: 'cache' + # # => 'cache:foo' + # + # With a namespace block: + # + # namespace_key 'foo', namespace: -> { 'cache' } + # # => 'cache:foo' + def namespace_key(key, options = nil) + options = merged_options(options) + namespace = options[:namespace] + + if namespace.respond_to?(:call) + namespace = namespace.call + end + + if namespace + "#{namespace}:#{key}" + else + key + end + end + + # Expands key to be a consistent string value. Invokes +cache_key+ if + # object responds to +cache_key+. Otherwise, +to_param+ method will be + # called. If the key is a Hash, then keys will be sorted alphabetically. + def expanded_key(key) + return key.cache_key.to_s if key.respond_to?(:cache_key) + + case key + when Array + if key.size > 1 + key = key.collect { |element| expanded_key(element) } + else + key = key.first + end + when Hash + key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" } + end + + key.to_param end def normalize_version(key, options = nil) @@ -643,7 +692,7 @@ module ActiveSupport class Entry # :nodoc: attr_reader :version - DEFAULT_COMPRESS_LIMIT = 16.kilobytes + DEFAULT_COMPRESS_LIMIT = 1.kilobyte # Creates a new cache entry for the specified value. Options supported are # +:compress+, +:compress_threshold+, and +:expires_in+. @@ -718,8 +767,8 @@ module ActiveSupport private def should_compress?(value, options) - if value && options[:compress] - compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT + if value && options.fetch(:compress, true) + compress_threshold = options.fetch(:compress_threshold, DEFAULT_COMPRESS_LIMIT) serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize return true if serialized_value_size >= compress_threshold diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 0812cc34c7..a0f44aac0f 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -121,7 +121,7 @@ module ActiveSupport fname = URI.encode_www_form_component(key) if fname.size > FILEPATH_MAX_SIZE - fname = Digest::MD5.hexdigest(key) + fname = ActiveSupport::Digest.hexdigest(key) end hash = Zlib.adler32(fname) diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 9242a334f2..cae0d44e7d 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -7,7 +7,6 @@ rescue LoadError => e raise e end -require "digest/md5" require "active_support/core_ext/marshal" require "active_support/core_ext/array/extract_options" @@ -64,7 +63,23 @@ module ActiveSupport addresses = addresses.flatten options = addresses.extract_options! addresses = ["localhost:11211"] if addresses.empty? - Dalli::Client.new(addresses, options) + + pool_options = {} + pool_options[:size] = options[:pool_size] if options[:pool_size] + pool_options[:timeout] = options[:pool_timeout] if options[:pool_timeout] + + if pool_options.empty? + Dalli::Client.new(addresses, options) + else + begin + require "connection_pool" + rescue LoadError => e + $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install" + raise e + end + + ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) } + end end # Creates a new MemCacheStore object, with the given memcached server @@ -92,28 +107,6 @@ module ActiveSupport end end - # Reads multiple values from the cache using a single call to the - # servers for all keys. Options can be passed in the last argument. - def read_multi(*names) - options = names.extract_options! - options = merged_options(options) - - keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }] - - raw_values = @data.get_multi(keys_to_names.keys) - values = {} - - raw_values.each do |key, value| - entry = deserialize_entry(value) - - unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) - values[keys_to_names[key]] = entry.value - end - end - - values - end - # Increment a cached value. This method uses the memcached incr atomic # operator and can only be used on values written with the :raw option. # Calling it on a value not stored with :raw will initialize that value @@ -122,7 +115,7 @@ module ActiveSupport options = merged_options(options) instrument(:increment, name, amount: amount) do rescue_error_with nil do - @data.incr(normalize_key(name, options), amount) + @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) } end end end @@ -135,7 +128,7 @@ module ActiveSupport options = merged_options(options) instrument(:decrement, name, amount: amount) do rescue_error_with nil do - @data.decr(normalize_key(name, options), amount) + @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) } end end end @@ -143,18 +136,18 @@ module ActiveSupport # Clear the entire cache on all memcached servers. This method should # be used with care when shared cache is being used. def clear(options = nil) - rescue_error_with(nil) { @data.flush_all } + rescue_error_with(nil) { @data.with { |c| c.flush_all } } end # Get the statistics from the memcached servers. def stats - @data.stats + @data.with { |c| c.stats } end private # Read an entry from the cache. def read_entry(key, options) - rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) } + rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) } end # Write an entry to the cache. @@ -167,13 +160,31 @@ module ActiveSupport expires_in += 5.minutes end rescue_error_with false do - @data.send(method, key, value, expires_in, options) + @data.with { |c| c.send(method, key, value, expires_in, options) } + end + end + + # Reads multiple entries from the cache implementation. + def read_multi_entries(names, options) + keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }] + + raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) } + values = {} + + raw_values.each do |key, value| + entry = deserialize_entry(value) + + unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) + values[keys_to_names[key]] = entry.value + end end + + values end # Delete an entry from the cache. def delete_entry(key, options) - rescue_error_with(false) { @data.delete(key) } + rescue_error_with(false) { @data.with { |c| c.delete(key) } } end # Memcache keys are binaries. So we need to force their encoding to binary @@ -183,7 +194,7 @@ module ActiveSupport key = super.dup key = key.force_encoding(Encoding::ASCII_8BIT) key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" } - key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 + key = "#{key[0, 213]}:md5:#{ActiveSupport::Digest.hexdigest(key)}" if key.size > 250 key end diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb new file mode 100644 index 0000000000..af14c28408 --- /dev/null +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -0,0 +1,436 @@ +# frozen_string_literal: true + +begin + gem "redis", ">= 4.0.1" + require "redis" + require "redis/distributed" +rescue LoadError + warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`" + raise +end + +# Prefer the hiredis driver but don't require it. +begin + require "redis/connection/hiredis" +rescue LoadError +end + +require "digest/sha2" +require "active_support/core_ext/marshal" +require "active_support/core_ext/hash/transform_values" + +module ActiveSupport + module Cache + # Redis cache store. + # + # Deployment note: Take care to use a *dedicated Redis cache* rather + # than pointing this at your existing Redis server. It won't cope well + # with mixed usage patterns and it won't expire cache entries by default. + # + # Redis cache server setup guide: https://redis.io/topics/lru-cache + # + # * Supports vanilla Redis, hiredis, and Redis::Distributed. + # * Supports Memcached-like sharding across Redises with Redis::Distributed. + # * Fault tolerant. If the Redis server is unavailable, no exceptions are + # raised. Cache fetches are all misses and writes are dropped. + # * Local cache. Hot in-memory primary cache within block/middleware scope. + # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed + # 4.0.1+ for distributed mget support. + # * +delete_matched+ support for Redis KEYS globs. + class RedisCacheStore < Store + # Keys are truncated with their own SHA2 digest if they exceed 1kB + MAX_KEY_BYTESIZE = 1024 + + DEFAULT_REDIS_OPTIONS = { + connect_timeout: 20, + read_timeout: 1, + write_timeout: 1, + reconnect_attempts: 0, + } + + DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do + if logger + logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" } + end + end + + DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end" + private_constant :DELETE_GLOB_LUA + + # Support raw values in the local cache strategy. + module LocalCacheWithRaw # :nodoc: + private + def read_entry(key, options) + entry = super + if options[:raw] && local_cache && entry + entry = deserialize_entry(entry.value) + end + entry + end + + def write_entry(key, entry, options) + if options[:raw] && local_cache + raw_entry = Entry.new(serialize_entry(entry, raw: true)) + raw_entry.expires_at = entry.expires_at + super(key, raw_entry, options) + else + super + end + end + + def write_multi_entries(entries, options) + if options[:raw] && local_cache + raw_entries = entries.map do |key, entry| + raw_entry = Entry.new(serialize_entry(entry, raw: true)) + raw_entry.expires_at = entry.expires_at + end.to_h + + super(raw_entries, options) + else + super + end + end + end + + prepend Strategy::LocalCache + prepend LocalCacheWithRaw + + class << self + # Factory method to create a new Redis instance. + # + # Handles four options: :redis block, :redis instance, single :url + # string, and multiple :url strings. + # + # Option Class Result + # :redis Proc -> options[:redis].call + # :redis Object -> options[:redis] + # :url String -> Redis.new(url: …) + # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …]) + # + def build_redis(redis: nil, url: nil, **redis_options) #:nodoc: + urls = Array(url) + + if redis.respond_to?(:call) + redis.call + elsif redis + redis + elsif urls.size > 1 + build_redis_distributed_client urls: urls, **redis_options + else + build_redis_client url: urls.first, **redis_options + end + end + + private + def build_redis_distributed_client(urls:, **redis_options) + ::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist| + urls.each { |u| dist.add_node url: u } + end + end + + def build_redis_client(url:, **redis_options) + ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url)) + end + end + + attr_reader :redis_options + attr_reader :max_key_bytesize + + # Creates a new Redis cache store. + # + # Handles three options: block provided to instantiate, single URL + # provided, and multiple URLs provided. + # + # :redis Proc -> options[:redis].call + # :url String -> Redis.new(url: …) + # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …]) + # + # No namespace is set by default. Provide one if the Redis cache + # server is shared with other apps: <tt>namespace: 'myapp-cache'<tt>. + # + # Compression is enabled by default with a 1kB threshold, so cached + # values larger than 1kB are automatically compressed. Disable by + # passing <tt>cache: false</tt> or change the threshold by passing + # <tt>compress_threshold: 4.kilobytes</tt>. + # + # No expiry is set on cache entries by default. Redis is expected to + # be configured with an eviction policy that automatically deletes + # least-recently or -frequently used keys when it reaches max memory. + # See https://redis.io/topics/lru-cache for cache server setup. + # + # Race condition TTL is not set by default. This can be used to avoid + # "thundering herd" cache writes when hot cache entries are expired. + # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more. + def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options) + @redis_options = redis_options + + @max_key_bytesize = MAX_KEY_BYTESIZE + @error_handler = error_handler + + super namespace: namespace, + compress: compress, compress_threshold: compress_threshold, + expires_in: expires_in, race_condition_ttl: race_condition_ttl + end + + def redis + @redis ||= self.class.build_redis(**redis_options) + end + + def inspect + instance = @redis || @redis_options + "<##{self.class} options=#{options.inspect} redis=#{instance.inspect}>" + end + + # Cache Store API implementation. + # + # Read multiple values at once. Returns a hash of requested keys -> + # fetched values. + def read_multi(*names) + if mget_capable? + instrument(:read_multi, names, options) do |payload| + read_multi_mget(*names).tap do |results| + payload[:hits] = results.keys + end + end + else + super + end + end + + # Cache Store API implementation. + # + # Supports Redis KEYS glob patterns: + # + # h?llo matches hello, hallo and hxllo + # h*llo matches hllo and heeeello + # h[ae]llo matches hello and hallo, but not hillo + # h[^e]llo matches hallo, hbllo, ... but not hello + # h[a-b]llo matches hallo and hbllo + # + # Use \ to escape special characters if you want to match them verbatim. + # + # See https://redis.io/commands/KEYS for more. + # + # Failsafe: Raises errors. + def delete_matched(matcher, options = nil) + instrument :delete_matched, matcher do + case matcher + when String + redis.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] + else + raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" + end + end + end + + # Cache Store API implementation. + # + # Increment a cached value. This method uses the Redis incr atomic + # operator and can only be used on values written with the :raw option. + # Calling it on a value not stored with :raw will initialize that value + # to zero. + # + # Failsafe: Raises errors. + def increment(name, amount = 1, options = nil) + instrument :increment, name, amount: amount do + failsafe :increment do + redis.incrby normalize_key(name, options), amount + end + end + end + + # Cache Store API implementation. + # + # Decrement a cached value. This method uses the Redis decr atomic + # operator and can only be used on values written with the :raw option. + # Calling it on a value not stored with :raw will initialize that value + # to zero. + # + # Failsafe: Raises errors. + def decrement(name, amount = 1, options = nil) + instrument :decrement, name, amount: amount do + failsafe :decrement do + redis.decrby normalize_key(name, options), amount + end + end + end + + # Cache Store API implementation. + # + # Removes expired entries. Handled natively by Redis least-recently-/ + # least-frequently-used expiry, so manual cleanup is not supported. + def cleanup(options = nil) + super + end + + # Clear the entire cache on all Redis servers. Safe to use on + # shared servers if the cache is namespaced. + # + # Failsafe: Raises errors. + def clear(options = nil) + failsafe :clear do + if namespace = merged_options(options)[namespace] + delete_matched "*", namespace: namespace + else + redis.flushdb + end + end + end + + def mget_capable? #:nodoc: + set_redis_capabilities unless defined? @mget_capable + @mget_capable + end + + def mset_capable? #:nodoc: + set_redis_capabilities unless defined? @mset_capable + @mset_capable + end + + private + def set_redis_capabilities + case redis + when Redis::Distributed + @mget_capable = true + @mset_capable = false + else + @mget_capable = true + @mset_capable = true + end + end + + # Store provider interface: + # Read an entry from the cache. + def read_entry(key, options = nil) + failsafe :read_entry do + deserialize_entry redis.get(key) + end + end + + def read_multi_entries(names, _options) + if mget_capable? + read_multi_mget(*names) + else + super + end + end + + def read_multi_mget(*names) + options = names.extract_options! + options = merged_options(options) + + keys = names.map { |name| normalize_key(name, options) } + + values = failsafe(:read_multi_mget, returning: {}) do + redis.mget(*keys) + end + + names.zip(values).each_with_object({}) do |(name, value), results| + if value + entry = deserialize_entry(value) + unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options)) + results[name] = entry.value + end + end + end + end + + # Write an entry to the cache. + # + # Requires Redis 2.6.12+ for extended SET options. + def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options) + serialized_entry = serialize_entry(entry, raw: raw) + + # If race condition TTL is in use, ensure that cache entries + # stick around a bit longer after they would have expired + # so we can purposefully serve stale entries. + if race_condition_ttl && expires_in && expires_in > 0 && !raw + expires_in += 5.minutes + end + + failsafe :write_entry, returning: false do + if unless_exist || expires_in + modifiers = {} + modifiers[:nx] = unless_exist + modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in + + redis.set key, serialized_entry, modifiers + else + redis.set key, serialized_entry + end + end + end + + # Delete an entry from the cache. + def delete_entry(key, options) + failsafe :delete_entry, returning: false do + redis.del key + end + end + + # Nonstandard store provider API to write multiple values at once. + def write_multi_entries(entries, expires_in: nil, **options) + if entries.any? + if mset_capable? && expires_in.nil? + failsafe :write_multi_entries do + redis.mapped_mset(serialize_entries(entries, raw: options[:raw])) + end + else + super + end + end + end + + # Truncate keys that exceed 1kB. + def normalize_key(key, options) + truncate_key super.b + end + + def truncate_key(key) + if key.bytesize > max_key_bytesize + suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}" + truncate_at = max_key_bytesize - suffix.bytesize + "#{key.byteslice(0, truncate_at)}#{suffix}" + else + key + end + end + + def deserialize_entry(serialized_entry) + if serialized_entry + entry = Marshal.load(serialized_entry) rescue serialized_entry + entry.is_a?(Entry) ? entry : Entry.new(entry) + end + end + + def serialize_entry(entry, raw: false) + if raw + entry.value.to_s + else + Marshal.dump(entry) + end + end + + def serialize_entries(entries, raw: false) + entries.transform_values do |entry| + serialize_entry entry, raw: raw + end + end + + def failsafe(method, returning: nil) + yield + rescue ::Redis::BaseConnectionError => e + handle_exception exception: e, method: method, returning: returning + returning + end + + def handle_exception(exception:, method:, returning:) + if @error_handler + @error_handler.(method: method, exception: exception, returning: returning) + end + rescue => failsafe + warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}" + end + end + end +end diff --git a/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb b/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb new file mode 100644 index 0000000000..a8455c0048 --- /dev/null +++ b/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "monitor" + +module ActiveSupport + module Concurrency + # A monitor that will permit dependency loading while blocked waiting for + # the lock. + class LoadInterlockAwareMonitor < Monitor + # Enters an exclusive section, but allows dependency loading while blocked + def mon_enter + mon_try_enter || + ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super } + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index d67f99df0e..b7ff7a3907 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -35,8 +35,8 @@ class Array # people.without "Aaron", "Todd" # # => ["David", "Rafael"] # - # Note: This is an optimization of `Enumerable#without` that uses `Array#-` - # instead of `Array#reject` for performance reasons. + # Note: This is an optimization of <tt>Enumerable#without</tt> that uses <tt>Array#-</tt> + # instead of <tt>Array#reject</tt> for performance reasons. def without(*elements) self - elements end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index 061b79e098..f6cb1a384c 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb @@ -330,20 +330,28 @@ module DateAndTime beginning_of_year..end_of_year end - # Returns specific next occurring day of week + # Returns a new date/time representing the next occurrence of the specified day of week. + # + # today = Date.today # => Thu, 14 Dec 2017 + # today.next_occurring(:monday) # => Mon, 18 Dec 2017 + # today.next_occurring(:thursday) # => Thu, 21 Dec 2017 def next_occurring(day_of_week) current_day_number = wday != 0 ? wday - 1 : 6 from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number from_now += 7 unless from_now > 0 - since(from_now.days) + advance(days: from_now) end - # Returns specific previous occurring day of week + # Returns a new date/time representing the previous occurrence of the specified day of week. + # + # today = Date.today # => Thu, 14 Dec 2017 + # today.prev_occurring(:monday) # => Mon, 11 Dec 2017 + # today.prev_occurring(:thursday) # => Thu, 07 Dec 2017 def prev_occurring(day_of_week) current_day_number = wday != 0 ? wday - 1 : 6 ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week) ago += 7 unless ago > 0 - ago(ago.days) + advance(days: -ago) end private diff --git a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb index 424f64d6fa..7600a067cc 100644 --- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb @@ -8,9 +8,9 @@ class DateTime silence_redefinition_of_method :to_time - # Either return an instance of `Time` with the same UTC offset - # as +self+ or an instance of `Time` representing the same time - # in the the local system timezone depending on the setting of + # Either return an instance of +Time+ with the same UTC offset + # as +self+ or an instance of +Time+ representing the same time + # in the local system timezone depending on the setting of # on the setting of +ActiveSupport.to_time_preserves_timezone+. def to_time preserve_timezone ? getlocal(utc_offset) : getlocal diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 11d28d12a1..5b48254646 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -165,7 +165,7 @@ module ActiveSupport Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ] when Array params.map { |v| normalize_keys(v) } - else + else params end end @@ -178,7 +178,7 @@ module ActiveSupport process_array(value) when String value - else + else raise "can't typecast #{value.class.name} - #{value.inspect}" end end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 370a948eea..7bbbf321ab 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -22,7 +22,7 @@ class Module # # == Using comments: # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -30,7 +30,6 @@ class Module # has_many :events # # before_create :track_creation - # after_destroy :track_deletion # # private # def track_creation @@ -42,7 +41,7 @@ class Module # # Noisy syntax. # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -52,7 +51,6 @@ class Module # included do # has_many :events # before_create :track_creation - # after_destroy :track_deletion # end # # private @@ -70,7 +68,7 @@ class Module # increased overhead can be a reasonable tradeoff even if it reduces our # at-a-glance perception of how things work. # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -82,7 +80,7 @@ class Module # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to # separate bite-sized concerns. # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -90,7 +88,6 @@ class Module # included do # has_many :events # before_create :track_creation - # after_destroy :track_deletion # end # # private @@ -101,7 +98,7 @@ class Module # end # # Todo.ancestors - # # => [Todo, Todo::EventTracking, Object] + # # => [Todo, Todo::EventTracking, ApplicationRecord, Object] # # This small step has some wonderful ripple effects. We can # * grok the behavior of our class in one glance, diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index a77f903db5..4310df3024 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -115,11 +115,8 @@ class Module # invoice.customer_address # => 'Vimmersvej 13' # # If the target is +nil+ and does not respond to the delegated method a - # +Module::DelegationError+ is raised, as with any other value. Sometimes, - # however, it makes sense to be robust to that situation and that is the - # purpose of the <tt>:allow_nil</tt> option: If the target is not +nil+, or it - # is and responds to the method, everything works as usual. But if it is +nil+ - # and does not respond to the delegated method, +nil+ is returned. + # +Module::DelegationError+ is raised. If you wish to instead return +nil+, + # use the <tt>:allow_nil</tt> option. # # class User < ActiveRecord::Base # has_one :profile diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb index d4f1e01140..6d37cd9dfd 100644 --- a/activesupport/lib/active_support/core_ext/name_error.rb +++ b/activesupport/lib/active_support/core_ext/name_error.rb @@ -10,6 +10,11 @@ class NameError # end # # => "HelloWorld" def missing_name + # Since ruby v2.3.0 `did_you_mean` gem is loaded by default. + # It extends NameError#message with spell corrections which are SLOW. + # We should use original_message message instead. + message = respond_to?(:original_message) ? original_message : self.message + if /undefined local variable or method/ !~ message $1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message end diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index e42ad852dd..2ca431ab10 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/regexp" +require "concurrent/map" class Object # An object is blank if it's false, empty, or a whitespace string. @@ -102,6 +103,9 @@ end class String BLANK_RE = /\A[[:space:]]*\z/ + ENCODED_BLANKS = Concurrent::Map.new do |h, enc| + h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING) + end # A string is blank if it's empty or contains whitespaces only: # @@ -119,7 +123,12 @@ class String # The regexp that matches blank strings is expensive. For the case of empty # strings we can speed up this method (~3.5x) with an empty? call. The # penalty for the rest of strings is marginal. - empty? || BLANK_RE.match?(self) + empty? || + begin + BLANK_RE.match?(self) + rescue Encoding::CompatibilityError + ENCODED_BLANKS[self.encoding].match?(self) + end end end diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 1744a44df6..9bb99087bc 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -108,8 +108,8 @@ require "bigdecimal" class BigDecimal # BigDecimals are duplicable: # - # BigDecimal.new("1.2").duplicable? # => true - # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)> + # BigDecimal("1.2").duplicable? # => true + # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)> def duplicable? true end diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index b6c464db33..2838fd76be 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -62,7 +62,7 @@ class Object # # validates :content, length: { minimum: 50 }, if: -> { content.present? } # - # Hence the inherited default for `if` key is ignored. + # Hence the inherited default for +if+ key is ignored. # # NOTE: You cannot call class methods implicitly inside of with_options. # You can access these methods using the class name instead: diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index da53739efc..8af301734a 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -174,7 +174,7 @@ class String # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> # - # To preserve the case of the characters in a string, use the `preserve_case` argument. + # To preserve the case of the characters in a string, use the +preserve_case+ argument. # # class Person # def to_param diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 38224ea5da..07c0d16398 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -16,6 +16,8 @@ class String # >> "lj".mb_chars.upcase.to_s # => "LJ" # + # NOTE: An above example is useful for pre Ruby 2.4. Ruby 2.4 supports Unicode case mappings. + # # == Method chaining # # All the methods on the Chars proxy which normally return a string will return a Chars object. This allows 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 b712200959..f3bdc2977e 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -250,7 +250,7 @@ class String # Marks a string as trusted safe. It will be inserted into HTML with no # additional escaping performed. It is your responsibility to ensure that the # string contains no malicious content. This method is equivalent to the - # `raw` helper in views. It is recommended that you use `sanitize` instead of + # +raw+ helper in views. It is recommended that you use +sanitize+ instead of # this method. It should never be called on user input. def html_safe ActiveSupport::SafeBuffer.new(self) diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 82c10b3079..0f59558bb5 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -447,6 +447,7 @@ module ActiveSupport #:nodoc: mod = Module.new into.const_set const_name, mod autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path) + autoloaded_constants.uniq! mod end @@ -670,7 +671,7 @@ module ActiveSupport #:nodoc: when Module desc.name || raise(ArgumentError, "Anonymous modules have no name to be referenced by") - else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" + else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" end end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index a1ad2ca465..7271ab565b 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -35,7 +35,7 @@ module ActiveSupport # and the second is a library name. # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = "6.0", gem_name = "Rails") + def initialize(deprecation_horizon = "6.1", gem_name = "Rails") self.gem_name = gem_name self.deprecation_horizon = deprecation_horizon # By default, warnings are not silenced and debugging is off. diff --git a/activesupport/lib/active_support/deprecation/constant_accessor.rb b/activesupport/lib/active_support/deprecation/constant_accessor.rb index 3d7eedf637..1ed0015812 100644 --- a/activesupport/lib/active_support/deprecation/constant_accessor.rb +++ b/activesupport/lib/active_support/deprecation/constant_accessor.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/inflector/methods" - module ActiveSupport class Deprecation # DeprecatedConstantAccessor transforms a constant into a deprecated one by @@ -17,7 +15,7 @@ module ActiveSupport # # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) # - # (In a later update, the original implementation of `PLANETS` has been removed.) + # # (In a later update, the original implementation of `PLANETS` has been removed.) # # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) # include ActiveSupport::Deprecation::DeprecatedConstantAccessor @@ -29,6 +27,8 @@ module ActiveSupport # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] module DeprecatedConstantAccessor def self.included(base) + require "active_support/inflector/methods" + extension = Module.new do def const_missing(missing_const_name) if class_variable_defined?(:@@_deprecated_constants) diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index d359992bf5..5be893d281 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -8,9 +8,7 @@ module ActiveSupport module MethodWrapper # Declare that a method has been deprecated. # - # module Fred - # extend self - # + # class Fred # def aaa; end # def bbb; end # def ccc; end @@ -22,15 +20,15 @@ module ActiveSupport # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead') # # => Fred # - # Fred.aaa + # Fred.new.aaa # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10) # # => nil # - # Fred.bbb + # Fred.new.bbb # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11) # # => nil # - # Fred.ccc + # Fred.new.ccc # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12) # # => nil # @@ -39,7 +37,7 @@ module ActiveSupport # ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator) # # => [:ddd] # - # Fred.ddd + # Fred.new.ddd # DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15) # # => nil # @@ -48,7 +46,7 @@ module ActiveSupport # custom_deprecator.deprecate_methods(Fred, eee: :zzz) # # => [:eee] # - # Fred.eee + # Fred.new.eee # DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18) # # => nil def deprecate_methods(target_module, *method_names) @@ -62,6 +60,13 @@ module ActiveSupport deprecator.deprecation_warning(method_name, options[method_name]) super(*args, &block) end + + case + when target_module.protected_method_defined?(method_name) + protected method_name + when target_module.private_method_defined?(method_name) + private method_name + end end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index f6c6648917..896c0d2d8e 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/inflector/methods" require "active_support/core_ext/regexp" module ActiveSupport @@ -114,7 +113,7 @@ module ActiveSupport # # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) # - # (In a later update, the original implementation of `PLANETS` has been removed.) + # # (In a later update, the original implementation of `PLANETS` has been removed.) # # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') @@ -125,6 +124,8 @@ module ActiveSupport # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.") + require "active_support/inflector/methods" + @old_const = old_const @new_const = new_const @deprecator = deprecator diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 242e21b782..2c004f4c9e 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -61,7 +61,7 @@ module ActiveSupport case message when Symbol then "#{warning} (use #{message} instead)" when String then "#{warning} (#{message})" - else warning + else warning end end diff --git a/activesupport/lib/active_support/digest.rb b/activesupport/lib/active_support/digest.rb new file mode 100644 index 0000000000..fba10fbdcf --- /dev/null +++ b/activesupport/lib/active_support/digest.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ActiveSupport + class Digest #:nodoc: + class <<self + def hash_digest_class + @hash_digest_class ||= ::Digest::MD5 + end + + def hash_digest_class=(klass) + raise ArgumentError, "#{klass} is expected to implement hexdigest class method" unless klass.respond_to?(:hexdigest) + @hash_digest_class = klass + end + + def hexdigest(arg) + hash_digest_class.hexdigest(arg)[0...32] + end + end + end +end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 1af3411a8a..88897f811e 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -194,7 +194,6 @@ module ActiveSupport end parts[:seconds] = remainder - parts.reject! { |k, v| v.zero? } new(value, parts) end @@ -211,6 +210,7 @@ module ActiveSupport def initialize(value, parts) #:nodoc: @value, @parts = value, parts.to_h @parts.default = 0 + @parts.reject! { |k, v| v.zero? } end def coerce(other) #:nodoc: @@ -370,6 +370,8 @@ module ActiveSupport alias :before :ago def inspect #:nodoc: + return "0 seconds" if parts.empty? + parts. reduce(::Hash.new(0)) { |h, (l, r)| h[l] += r; h }. sort_by { |unit, _ | PARTS.index(unit) }. @@ -381,6 +383,14 @@ module ActiveSupport to_i end + def init_with(coder) #:nodoc: + initialize(coder["value"], coder["parts"]) + end + + def encode_with(coder) #:nodoc: + coder.map = { "value" => @value, "parts" => @parts } + end + # Build ISO 8601 Duration string for this duration. # The +precision+ parameter can be used to limit seconds' precision of duration. def iso8601(precision: nil) diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb index b403048627..dab953d5d5 100644 --- a/activesupport/lib/active_support/encrypted_configuration.rb +++ b/activesupport/lib/active_support/encrypted_configuration.rb @@ -11,8 +11,9 @@ module ActiveSupport delegate :[], :fetch, to: :config delegate_missing_to :options - def initialize(config_path:, key_path:, env_key:) - super content_path: config_path, key_path: key_path, env_key: env_key + def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:) + super content_path: config_path, key_path: key_path, + env_key: env_key, raise_if_missing_key: raise_if_missing_key end # Allow a config to be started without a file present @@ -22,6 +23,12 @@ module ActiveSupport "" end + def write(contents) + deserialize(contents) + + super + end + def config @config ||= deserialize(read).deep_symbolize_keys end @@ -36,7 +43,7 @@ module ActiveSupport end def deserialize(config) - config.present? ? YAML.load(config) : {} + config.present? ? YAML.load(config, content_path) : {} end end end diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb index 3d1455fb95..671b6b6a69 100644 --- a/activesupport/lib/active_support/encrypted_file.rb +++ b/activesupport/lib/active_support/encrypted_file.rb @@ -26,11 +26,11 @@ module ActiveSupport end - attr_reader :content_path, :key_path, :env_key + attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key - def initialize(content_path:, key_path:, env_key:) + def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:) @content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path) - @env_key = env_key + @env_key, @raise_if_missing_key = env_key, raise_if_missing_key end def key @@ -38,7 +38,7 @@ module ActiveSupport end def read - if content_path.exist? + if !key.nil? && content_path.exist? decrypt content_path.binread else raise MissingContentError, content_path @@ -93,7 +93,7 @@ module ActiveSupport end def handle_missing_key - raise MissingKeyError, key_path: key_path, env_key: env_key + raise MissingKeyError, key_path: key_path, env_key: env_key if raise_if_missing_key end end end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 2a7ef2f820..c951ad16a3 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -7,8 +7,8 @@ module ActiveSupport end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index a639eddf10..7e5dff1d6d 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -4,6 +4,7 @@ require "concurrent/map" require "active_support/core_ext/array/prepend_and_append" require "active_support/core_ext/regexp" require "active_support/i18n" +require "active_support/deprecation" module ActiveSupport module Inflector @@ -67,16 +68,21 @@ module ActiveSupport end attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex + deprecate :acronym_regex + + attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc: def initialize - @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/ + @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {} + define_acronym_regex_patterns end # Private, for the test suite. def initialize_dup(orig) # :nodoc: - %w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope| + %w(plurals singulars uncountables humans acronyms).each do |scope| instance_variable_set("@#{scope}", orig.send(scope).dup) end + define_acronym_regex_patterns end # Specifies a new acronym. An acronym must be specified as it will appear @@ -130,7 +136,7 @@ module ActiveSupport # camelize 'mcdonald' # => 'McDonald' def acronym(word) @acronyms[word.downcase] = word - @acronym_regex = /#{@acronyms.values.join("|")}/ + define_acronym_regex_patterns end # Specifies a new pluralization rule and its replacement. The rule can @@ -221,10 +227,18 @@ module ActiveSupport case scope when :all @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, [] - else + else instance_variable_set "@#{scope}", [] end end + + private + + def define_acronym_regex_patterns + @acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/ + @acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/ + @acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/ + end end # Yields a singleton instance of Inflector::Inflections so you can specify diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 3357b5eb32..7e782e2a93 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -71,7 +71,7 @@ module ActiveSupport if uppercase_first_letter string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize } else - string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase } + 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) @@ -92,7 +92,7 @@ module ActiveSupport 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!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" } + 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) @@ -350,7 +350,7 @@ module ActiveSupport when 1; "st" when 2; "nd" when 3; "rd" - else "th" + else "th" end end end diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 9fb3a2e0af..6f2ca4999c 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -73,12 +73,12 @@ module ActiveSupport # parameterize("Donald E. Knuth") # => "donald-e-knuth" # parameterize("^très|Jolie-- ") # => "tres-jolie" # - # To use a custom separator, override the `separator` argument. + # To use a custom separator, override the +separator+ argument. # # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie" # - # To preserve the case of the characters in a string, use the `preserve_case` argument. + # To preserve the case of the characters in a string, use the +preserve_case+ argument. # # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" # parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie" diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 69c95e0622..5236c776dd 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -58,8 +58,8 @@ module ActiveSupport # === Rotating keys # # MessageEncryptor also supports rotating out old configurations by falling - # back to a stack of encryptors. Call `rotate` to build and add an encryptor - # so `decrypt_and_verify` will also try the fallback. + # back to a stack of encryptors. Call +rotate+ to build and add an encryptor + # so +decrypt_and_verify+ will also try the fallback. # # By default any rotated encryptors use the values of the primary # encryptor unless specified otherwise. @@ -81,9 +81,9 @@ module ActiveSupport class MessageEncryptor prepend Messages::Rotator::Encryptor - class << self - attr_accessor :use_authenticated_message_encryption #:nodoc: + cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false + class << self def default_cipher #:nodoc: if use_authenticated_message_encryption "aes-256-gcm" diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 622b66ee55..83c39c0a86 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -31,7 +31,7 @@ module ActiveSupport # # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default. # If you want to use a different hash algorithm, you can change it by providing - # `:digest` key as an option while initializing the verifier: + # +:digest+ key as an option while initializing the verifier: # # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256') # @@ -78,8 +78,8 @@ module ActiveSupport # === Rotating keys # # MessageVerifier also supports rotating out old configurations by falling - # back to a stack of verifiers. Call `rotate` to build and add a verifier to - # so either `verified` or `verify` will also try verifying with the fallback. + # back to a stack of verifiers. Call +rotate+ to build and add a verifier to + # so either +verified+ or +verify+ will also try verifying with the fallback. # # By default any rotated verifiers use the values of the primary # verifier unless specified otherwise. diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index a64223c0e0..f923061fae 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -276,7 +276,7 @@ module ActiveSupport reorder_characters(decompose(:compatibility, codepoints)) when :kc compose(reorder_characters(decompose(:compatibility, codepoints))) - else + else raise ArgumentError, "#{form} is not a valid normalization variant", caller end.pack("U*".freeze) end diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb index a5b28296a2..2ad8d49c4e 100644 --- a/activesupport/lib/active_support/number_helper/rounding_helper.rb +++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb @@ -36,7 +36,7 @@ module ActiveSupport return 0 if number.zero? digits = digit_count(number) multiplier = 10**(digits - precision) - (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier + (number / BigDecimal(multiplier.to_f.to_s)).round * multiplier end def convert_to_decimal(number) diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 8560eae110..605b50d346 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -10,9 +10,11 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport initializer "active_support.set_authenticated_message_encryption" do |app| - if app.config.active_support.respond_to?(:use_authenticated_message_encryption) - ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = - app.config.active_support.use_authenticated_message_encryption + config.after_initialize do + unless app.config.active_support.use_authenticated_message_encryption.nil? + ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = + app.config.active_support.use_authenticated_message_encryption + end end end @@ -66,5 +68,13 @@ module ActiveSupport ActiveSupport.send(k, v) if ActiveSupport.respond_to? k end end + + initializer "active_support.set_hash_digest_class" do |app| + config.after_initialize do + if app.config.active_support.use_sha1_digests + ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1 + end + end + end end end diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index 6cd11b1a26..b26d9c3665 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -28,14 +28,17 @@ module ActiveSupport define_callbacks :class_unload + # Registers a callback that will run once at application startup and every time the code is reloaded. def self.to_prepare(*args, &block) set_callback(:prepare, *args, &block) end + # Registers a callback that will run immediately before the classes are unloaded. def self.before_class_unload(*args, &block) set_callback(:class_unload, *args, &block) end + # Registers a callback that will run immediately after the classes are unloaded. def self.after_class_unload(*args, &block) set_callback(:class_unload, :after, *args, &block) end diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb index b6b31ef140..20b6b9cd3f 100644 --- a/activesupport/lib/active_support/security_utils.rb +++ b/activesupport/lib/active_support/security_utils.rb @@ -4,14 +4,12 @@ require "digest/sha2" module ActiveSupport module SecurityUtils - # Constant time string comparison. + # Constant time string comparison, for fixed length strings. # # The values compared should be of fixed length, such as strings - # that have already been processed by HMAC. This should not be used - # on variable length plaintext strings because it could leak length info - # via timing attacks. - def secure_compare(a, b) - return false unless a.bytesize == b.bytesize + # that have already been processed by HMAC. Raises in case of length mismatch. + def fixed_length_secure_compare(a, b) + raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize l = a.unpack "C#{a.bytesize}" @@ -19,11 +17,15 @@ module ActiveSupport b.each_byte { |byte| res |= byte ^ l.shift } res == 0 end - module_function :secure_compare + module_function :fixed_length_secure_compare - def variable_size_secure_compare(a, b) # :nodoc: - secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) + # Constant time string comparison, for variable length strings. + # + # The values are first processed by SHA256, so that we don't leak length info + # via timing attacks. + def secure_compare(a, b) + fixed_length_secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) && a == b end - module_function :variable_size_secure_compare + module_function :secure_compare end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index b24aa36ede..a891ff616d 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -58,6 +58,12 @@ module ActiveSupport # post :create, params: { article: {...} } # end # + # A hash of expressions/numeric differences can also be passed in and evaluated. + # + # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do + # post :create, params: { article: {...} } + # end + # # A lambda or a list of lambdas can be passed in and evaluated: # # assert_difference ->{ Article.count }, 2 do @@ -73,20 +79,28 @@ module ActiveSupport # assert_difference 'Article.count', -1, 'An Article should be destroyed' do # post :delete, params: { id: ... } # end - def assert_difference(expression, difference = 1, message = nil, &block) - expressions = Array(expression) - - exps = expressions.map { |e| + def assert_difference(expression, *args, &block) + expressions = + if expression.is_a?(Hash) + message = args[0] + expression + else + difference = args[0] || 1 + message = args[1] + Hash[Array(expression).map { |e| [e, difference] }] + end + + exps = expressions.keys.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } before = exps.map(&:call) retval = yield - expressions.zip(exps).each_with_index do |(code, e), i| - error = "#{code.inspect} didn't change by #{difference}" + expressions.zip(exps, before) do |(code, diff), exp, before_value| + error = "#{code.inspect} didn't change by #{diff}" error = "#{message}.\n#{error}" if message - assert_equal(before[i] + difference, e.call, error) + assert_equal(before_value + diff, exp.call, error) end retval @@ -156,11 +170,12 @@ module ActiveSupport after = exp.call - if to == UNTRACKED - error = "#{expression.inspect} didn't change" - error = "#{message}.\n#{error}" if message - assert before != after, error - else + error = "#{expression.inspect} didn't change" + error = "#{error}. It was already #{to}" if before == to + error = "#{message}.\n#{error}" if message + assert before != after, error + + unless to == UNTRACKED error = "#{expression.inspect} didn't change to #{to}" error = "#{message}.\n#{error}" if message assert to === after, error diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index fa9bebb181..562f985f1b 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -45,7 +45,8 @@ module ActiveSupport end } end - result = Marshal.dump(dup) + test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup + result = Marshal.dump(test_result) end write.puts [result].pack("m") @@ -69,8 +70,9 @@ module ActiveSupport if ENV["ISOLATION_TEST"] yield + test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup File.open(ENV["ISOLATION_OUTPUT"], "w") do |file| - file.puts [Marshal.dump(dup)].pack("m") + file.puts [Marshal.dump(test_result)].pack("m") end exit! else diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 8c620e7f8c..998a51a34c 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/string/strip" # for strip_heredoc require "active_support/core_ext/time/calculations" require "concurrent/map" @@ -43,7 +44,7 @@ module ActiveSupport def unstub_object(stub) singleton_class = stub.object.singleton_class - singleton_class.send :undef_method, stub.method_name + singleton_class.send :silence_redefinition_of_method, stub.method_name singleton_class.send :alias_method, stub.method_name, stub.original_method singleton_class.send :undef_method, stub.original_method end diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 4d1fbd4453..b75f5733b5 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -30,7 +30,7 @@ module ActiveSupport class TimeZone # Keys are Rails TimeZone names, values are TZInfo identifiers. MAPPING = { - "International Date Line West" => "Pacific/Midway", + "International Date Line West" => "Etc/GMT+12", "Midway Island" => "Pacific/Midway", "American Samoa" => "Pacific/Pago_Pago", "Hawaii" => "Pacific/Honolulu", @@ -238,7 +238,7 @@ module ActiveSupport when Numeric, ActiveSupport::Duration arg *= 3600 if arg.abs <= 13 all.find { |z| z.utc_offset == arg.to_i } - else + else raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" end end @@ -256,6 +256,13 @@ module ActiveSupport @country_zones[code] ||= load_country_zones(code) end + def clear #:nodoc: + @lazy_zones_map = Concurrent::Map.new + @country_zones = Concurrent::Map.new + @zones = nil + @zones_map = nil + end + private def load_country_zones(code) country = TZInfo::Country.get(code) @@ -269,9 +276,8 @@ module ActiveSupport end def zones_map - @zones_map ||= begin - MAPPING.each_key { |place| self[place] } # load all the zones - @lazy_zones_map + @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones| + zones[name] = self[name] end end end @@ -506,7 +512,7 @@ module ActiveSupport # Available so that TimeZone instances respond like TZInfo::Timezone # instances. def period_for_local(time, dst = true) - tzinfo.period_for_local(time, dst) + tzinfo.period_for_local(time, dst) { |periods| periods.last } end def periods_for_local(time) #:nodoc: diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 3ac11e0fe0..f214898145 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -38,4 +38,8 @@ class ActiveSupport::TestCase private 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/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb index d5419b862d..9a260edd8a 100644 --- a/activesupport/test/array_inquirer_test.rb +++ b/activesupport/test/array_inquirer_test.rb @@ -9,9 +9,9 @@ class ArrayInquirerTest < ActiveSupport::TestCase end def test_individual - assert @array_inquirer.mobile? - assert @array_inquirer.tablet? - assert_not @array_inquirer.desktop? + assert_predicate @array_inquirer, :mobile? + assert_predicate @array_inquirer, :tablet? + assert_not_predicate @array_inquirer, :desktop? end def test_any diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb index 424da0a52c..cb7a69cccf 100644 --- a/activesupport/test/benchmarkable_test.rb +++ b/activesupport/test/benchmarkable_test.rb @@ -26,7 +26,7 @@ class BenchmarkableTest < ActiveSupport::TestCase def test_without_block assert_raise(LocalJumpError) { benchmark } - assert buffer.empty? + assert_empty buffer end def test_defaults diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb index cb08a10bba..2d39976be3 100644 --- a/activesupport/test/cache/behaviors.rb +++ b/activesupport/test/cache/behaviors.rb @@ -3,7 +3,10 @@ require_relative "behaviors/autoloading_cache_behavior" require_relative "behaviors/cache_delete_matched_behavior" require_relative "behaviors/cache_increment_decrement_behavior" +require_relative "behaviors/cache_instrumentation_behavior" require_relative "behaviors/cache_store_behavior" require_relative "behaviors/cache_store_version_behavior" +require_relative "behaviors/connection_pool_behavior" require_relative "behaviors/encoded_key_cache_behavior" +require_relative "behaviors/failure_safety_behavior" require_relative "behaviors/local_cache_behavior" diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb index 2fa2d7af88..16b7abc679 100644 --- a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb @@ -8,7 +8,9 @@ module CacheIncrementDecrementBehavior assert_equal 2, @cache.read("foo").to_i assert_equal 3, @cache.increment("foo") assert_equal 3, @cache.read("foo").to_i - assert_nil @cache.increment("bar") + + missing = @cache.increment("bar") + assert(missing.nil? || missing == 1) end def test_decrement @@ -18,6 +20,8 @@ module CacheIncrementDecrementBehavior assert_equal 2, @cache.read("foo").to_i assert_equal 1, @cache.decrement("foo") assert_equal 1, @cache.read("foo").to_i - assert_nil @cache.decrement("bar") + + missing = @cache.decrement("bar") + assert(missing.nil? || missing == -1) end end diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb index 5b6fd678c5..4e8ff60eb3 100644 --- a/activesupport/test/cache/cache_store_write_multi_test.rb +++ b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb @@ -1,28 +1,15 @@ # frozen_string_literal: true -require "abstract_unit" -require "active_support/cache" - -class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - test "fetch_multi uses write_multi_entries store provider interface" do +module CacheInstrumentationBehavior + def test_fetch_multi_uses_write_multi_entries_store_provider_interface assert_called_with(@cache, :write_multi_entries) do @cache.fetch_multi "a", "b", "c" do |key| key * 2 end end end -end -class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - test "instrumentation" do + def test_write_multi_instrumentation writes = { "a" => "aa", "b" => "bb" } events = with_instrumentation "write_multi" do @@ -34,16 +21,27 @@ class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) end - test "instrumentation with fetch_multi as super operation" do - skip "fetch_multi isn't instrumented yet" + def test_instrumentation_with_fetch_multi_as_super_operation + @cache.write("b", "bb") - events = with_instrumentation "write_multi" do + events = with_instrumentation "read_multi" do @cache.fetch_multi("a", "b") { |key| key * 2 } end - assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) - assert_nil events[0].payload[:super_operation] - assert !events[0].payload[:hit] + assert_equal %w[ cache_read_multi.active_support ], events.map(&:name) + assert_equal :fetch_multi, events[0].payload[:super_operation] + assert_equal ["b"], events[0].payload[:hits] + end + + def test_read_multi_instrumentation + @cache.write("b", "bb") + + events = with_instrumentation "read_multi" do + @cache.read_multi("a", "b") { |key| key * 2 } + end + + assert_equal %w[ cache_read_multi.active_support ], events.map(&:name) + assert_equal ["b"], events[0].payload[:hits] end private diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index 582e902f72..efb57d34a2 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -113,6 +113,16 @@ module CacheStoreBehavior assert_equal("fufu", @cache.read("fu")) end + def test_fetch_multi_without_expires_in + @cache.write("foo", "bar") + @cache.write("fud", "biz") + + values = @cache.fetch_multi("foo", "fu", "fud", expires_in: nil) { |value| value * 2 } + + assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) + assert_equal("fufu", @cache.read("fu")) + end + def test_multi_with_objects cache_struct = Struct.new(:cache_key, :title) foo = cache_struct.new("foo", "FOO!") @@ -146,6 +156,16 @@ module CacheStoreBehavior assert_nil @cache.read("foo") end + def test_read_and_write_uncompressed_small_data + @cache.write("foo", "bar", compress: false) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_uncompressed_nil + @cache.write("foo", nil, compress: false) + assert_nil @cache.read("foo") + end + def test_cache_key obj = Object.new def obj.cache_key @@ -164,6 +184,18 @@ module CacheStoreBehavior assert_equal "bar", @cache.read("foo") end + def test_unversioned_cache_key + obj = Object.new + def obj.cache_key + "foo" + end + def obj.cache_key_with_version + "foo-v1" + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + def test_array_as_cache_key @cache.write([:fu, "foo"], "bar") assert_equal "bar", @cache.read("fu/foo") @@ -287,8 +319,7 @@ module CacheStoreBehavior end def test_really_long_keys - key = "".dup - 900.times { key << "x" } + key = "x" * 2048 assert @cache.write(key, "bar") assert_equal "bar", @cache.read(key) assert_equal "bar", @cache.fetch(key) diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb index c2e4d046af..62c0bb4f6f 100644 --- a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb @@ -65,7 +65,7 @@ module CacheStoreVersionBehavior m1v2 = ModelWithKeyAndVersion.new("model/1", 2) @cache.write(m1v1, "bar") - assert @cache.exist?(m1v1) + assert @cache.exist?(m1v1) assert_not @cache.fetch(m1v2) end diff --git a/activesupport/test/cache/behaviors/connection_pool_behavior.rb b/activesupport/test/cache/behaviors/connection_pool_behavior.rb new file mode 100644 index 0000000000..0d46f88552 --- /dev/null +++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ConnectionPoolBehavior + def test_connection_pool + Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + + emulating_latency do + begin + cache = ActiveSupport::Cache.lookup_store(store, pool_size: 2, pool_timeout: 1) + cache.clear + + threads = [] + + assert_raises Timeout::Error do + # One of the three threads will fail in 1 second because our pool size + # is only two. + 3.times do + threads << Thread.new do + cache.read("latency") + end + end + + threads.each(&:join) + end + ensure + threads.each(&:kill) + end + end + ensure + Thread.report_on_exception = original_report_on_exception if Thread.respond_to?(:report_on_exception) + end + + def test_no_connection_pool + emulating_latency do + begin + cache = ActiveSupport::Cache.lookup_store(store) + cache.clear + + threads = [] + + assert_nothing_raised do + # Default connection pool size is 5, assuming 10 will make sure that + # the connection pool isn't used at all. + 10.times do + threads << Thread.new do + cache.read("latency") + end + end + + threads.each(&:join) + end + ensure + threads.each(&:kill) + end + end + end +end diff --git a/activesupport/test/cache/behaviors/failure_safety_behavior.rb b/activesupport/test/cache/behaviors/failure_safety_behavior.rb new file mode 100644 index 0000000000..53bda4f942 --- /dev/null +++ b/activesupport/test/cache/behaviors/failure_safety_behavior.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module FailureSafetyBehavior + def test_fetch_read_failure_returns_nil + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_nil cache.fetch("foo") + end + end + + def test_fetch_read_failure_does_not_attempt_to_write + end + + def test_read_failure_returns_nil + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_nil cache.read("foo") + end + end + + def test_read_multi_failure_returns_empty_hash + @cache.write_multi("foo" => "bar", "baz" => "quux") + + emulating_unavailability do |cache| + assert_equal Hash.new, cache.read_multi("foo", "baz") + end + end + + def test_write_failure_returns_false + emulating_unavailability do |cache| + assert_equal false, cache.write("foo", "bar") + end + end + + def test_write_multi_failure_not_raises + emulating_unavailability do |cache| + assert_nothing_raised do + cache.write_multi("foo" => "bar", "baz" => "quux") + end + end + end + + def test_fetch_multi_failure_returns_fallback_results + @cache.write_multi("foo" => "bar", "baz" => "quux") + + emulating_unavailability do |cache| + fetched = cache.fetch_multi("foo", "baz") { |k| "unavailable" } + assert_equal Hash["foo" => "unavailable", "baz" => "unavailable"], fetched + end + end + + def test_delete_failure_returns_false + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_equal false, cache.delete("foo") + end + end + + def test_exist_failure_returns_false + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert !cache.exist?("foo") + end + end + + def test_increment_failure_returns_nil + @cache.write("foo", 1, raw: true) + + emulating_unavailability do |cache| + assert_nil cache.increment("foo") + end + end + + def test_decrement_failure_returns_nil + @cache.write("foo", 1, raw: true) + + emulating_unavailability do |cache| + assert_nil cache.decrement("foo") + end + end + + def test_clear_failure_returns_nil + emulating_unavailability do |cache| + assert_nil cache.clear + end + end +end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb index 8dec8090b1..f7302df4c8 100644 --- a/activesupport/test/cache/behaviors/local_cache_behavior.rb +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -20,7 +20,11 @@ module LocalCacheBehavior end def test_cleanup_clears_local_cache_but_not_remote_cache - skip unless @cache.class.instance_methods(false).include?(:cleanup) + begin + @cache.cleanup + rescue NotImplementedError + skip + end @cache.with_local_cache do @cache.write("foo", "bar") diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb index 51b214ad8f..80ff7ad564 100644 --- a/activesupport/test/cache/cache_entry_test.rb +++ b/activesupport/test/cache/cache_entry_test.rb @@ -14,16 +14,23 @@ class CacheEntryTest < ActiveSupport::TestCase end end - def test_compress_values + def test_compressed_values value = "value" * 100 entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) assert_equal value, entry.value assert(value.bytesize > entry.size, "value is compressed") end - def test_non_compress_values + def test_compressed_by_default value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value) + entry = ActiveSupport::Cache::Entry.new(value, compress_threshold: 1) + assert_equal value, entry.value + assert(value.bytesize > entry.size, "value is compressed") + end + + def test_uncompressed_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value, compress: false) assert_equal value, entry.value assert_equal value.bytesize, entry.size end diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb index 1af6893cc9..4648b2d361 100644 --- a/activesupport/test/cache/cache_store_logger_test.rb +++ b/activesupport/test/cache/cache_store_logger_test.rb @@ -13,7 +13,7 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase def test_logging @cache.fetch("foo") { "bar" } - assert @buffer.string.present? + assert_predicate @buffer.string, :present? end def test_log_with_string_namespace @@ -31,6 +31,6 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase def test_mute_logging @cache.mute { @cache.fetch("foo") { "bar" } } - assert @buffer.string.blank? + assert_predicate @buffer.string, :blank? end end diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb index 66231b0a82..c3c35a7bcc 100644 --- a/activesupport/test/cache/stores/file_store_test.rb +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -30,6 +30,7 @@ class FileStoreTest < ActiveSupport::TestCase include LocalCacheBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior include AutoloadingCacheBehavior def test_clear @@ -98,13 +99,13 @@ class FileStoreTest < ActiveSupport::TestCase end assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" - assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? + assert_empty Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) } end def test_log_exception_when_cache_read_fails File.stub(:exist?, -> { raise StandardError.new("failed") }) do @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? + assert_predicate @buffer.string, :present? end end diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb index 1b73fb65eb..f426a37c66 100644 --- a/activesupport/test/cache/stores/mem_cache_store_test.rb +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -5,6 +5,24 @@ require "active_support/cache" require_relative "../behaviors" require "dalli" +# Emulates a latency on Dalli's back-end for the key latency to facilitate +# connection pool testing. +class SlowDalliClient < Dalli::Client + def get(key, options = {}) + if key =~ /latency/ + sleep 3 + else + super + end + end +end + +class UnavailableDalliServer < Dalli::Server + def alive? + false + end +end + class MemCacheStoreTest < ActiveSupport::TestCase begin ss = Dalli::Client.new("localhost:11211").stats @@ -31,8 +49,11 @@ class MemCacheStoreTest < ActiveSupport::TestCase include CacheStoreVersionBehavior include LocalCacheBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior include EncodedKeyCacheBehavior include AutoloadingCacheBehavior + include ConnectionPoolBehavior + include FailureSafetyBehavior def test_raw_values cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) @@ -57,6 +78,22 @@ class MemCacheStoreTest < ActiveSupport::TestCase end end + def test_increment_expires_in + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + assert_called_with cache.instance_variable_get(:@data), :incr, [ "foo", 1, 60 ] do + cache.increment("foo", 1, expires_in: 60) + end + end + + def test_decrement_expires_in + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + assert_called_with cache.instance_variable_get(:@data), :decr, [ "foo", 1, 60 ] do + cache.decrement("foo", 1, expires_in: 60) + end + end + def test_local_cache_raw_values_with_marshal cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) cache.clear @@ -73,4 +110,30 @@ class MemCacheStoreTest < ActiveSupport::TestCase value << "bingo" assert_not_equal value, @cache.read("foo") end + + private + + def store + :mem_cache_store + end + + def emulating_latency + old_client = Dalli.send(:remove_const, :Client) + Dalli.const_set(:Client, SlowDalliClient) + + yield + ensure + Dalli.send(:remove_const, :Client) + Dalli.const_set(:Client, old_client) + end + + def emulating_unavailability + old_server = Dalli.send(:remove_const, :Server) + Dalli.const_set(:Server, UnavailableDalliServer) + + yield ActiveSupport::Cache::MemCacheStore.new + ensure + Dalli.send(:remove_const, :Server) + Dalli.const_set(:Server, old_server) + end end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb index 3981f05331..72fafc187b 100644 --- a/activesupport/test/cache/stores/memory_store_test.rb +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -14,6 +14,7 @@ class MemoryStoreTest < ActiveSupport::TestCase include CacheStoreVersionBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior def test_prune_size @cache.write(1, "aaaaaaaaaa") && sleep(0.001) diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb new file mode 100644 index 0000000000..9daa0b8796 --- /dev/null +++ b/activesupport/test/cache/stores/redis_cache_store_test.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/cache" +require "active_support/cache/redis_cache_store" +require_relative "../behaviors" + +module ActiveSupport::Cache::RedisCacheStoreTests + DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis" + + class LookupTest < ActiveSupport::TestCase + test "may be looked up as :redis_cache_store" do + assert_kind_of ActiveSupport::Cache::RedisCacheStore, + ActiveSupport::Cache.lookup_store(:redis_cache_store) + end + end + + class InitializationTest < ActiveSupport::TestCase + test "omitted URL uses Redis client with default settings" do + assert_called_with Redis, :new, [ + url: nil, + connect_timeout: 20, read_timeout: 1, write_timeout: 1, + reconnect_attempts: 0, driver: DRIVER + ] do + build + end + end + + test "no URLs uses Redis client with default settings" do + assert_called_with Redis, :new, [ + url: nil, + connect_timeout: 20, read_timeout: 1, write_timeout: 1, + reconnect_attempts: 0, driver: DRIVER + ] do + build url: [] + end + end + + test "singular URL uses Redis client" do + assert_called_with Redis, :new, [ + url: "redis://localhost:6379/0", + connect_timeout: 20, read_timeout: 1, write_timeout: 1, + reconnect_attempts: 0, driver: DRIVER + ] do + build url: "redis://localhost:6379/0" + end + end + + test "one URL uses Redis client" do + assert_called_with Redis, :new, [ + url: "redis://localhost:6379/0", + connect_timeout: 20, read_timeout: 1, write_timeout: 1, + reconnect_attempts: 0, driver: DRIVER + ] do + build url: %w[ redis://localhost:6379/0 ] + end + end + + test "multiple URLs uses Redis::Distributed client" do + assert_called_with Redis, :new, [ + [ url: "redis://localhost:6379/0", + connect_timeout: 20, read_timeout: 1, write_timeout: 1, + reconnect_attempts: 0, driver: DRIVER ], + [ url: "redis://localhost:6379/1", + connect_timeout: 20, read_timeout: 1, write_timeout: 1, + reconnect_attempts: 0, driver: DRIVER ], + ], returns: Redis.new do + @cache = build url: %w[ redis://localhost:6379/0 redis://localhost:6379/1 ] + assert_kind_of ::Redis::Distributed, @cache.redis + end + end + + test "block argument uses yielded client" do + block = -> { :custom_redis_client } + assert_called block, :call do + build redis: block + end + end + + private + def build(**kwargs) + ActiveSupport::Cache::RedisCacheStore.new(driver: DRIVER, **kwargs).tap do |cache| + cache.redis + end + end + end + + class StoreTest < ActiveSupport::TestCase + setup do + @namespace = "namespace" + + @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, expires_in: 60, driver: DRIVER) + # @cache.logger = Logger.new($stdout) # For test debugging + + # For LocalCacheBehavior tests + @peek = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, driver: DRIVER) + end + + teardown do + @cache.clear + @cache.redis.disconnect! + end + end + + class RedisCacheStoreCommonBehaviorTest < StoreTest + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior + include AutoloadingCacheBehavior + + def test_fetch_multi_uses_redis_mget + assert_called(@cache.redis, :mget, returns: []) do + @cache.fetch_multi("a", "b", "c") do |key| + key * 2 + end + end + end + end + + # Separate test class so we can omit the namespace which causes expected, + # appropriate complaints about incompatible string encodings. + class KeyEncodingSafetyTest < StoreTest + include EncodedKeyCacheBehavior + + setup do + @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, driver: DRIVER) + @cache.logger = nil + end + end + + class StoreAPITest < StoreTest + end + + class UnavailableRedisClient < Redis::Client + def ensure_connected + raise Redis::BaseConnectionError + end + end + + class FailureSafetyTest < StoreTest + include FailureSafetyBehavior + + private + + def emulating_unavailability + old_client = Redis.send(:remove_const, :Client) + Redis.const_set(:Client, UnavailableRedisClient) + + yield ActiveSupport::Cache::RedisCacheStore.new + ensure + Redis.send(:remove_const, :Client) + Redis.const_set(:Client, old_client) + end + end + + class DeleteMatchedTest < StoreTest + test "deletes keys matching glob" do + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.delete_matched("foo*") + assert !@cache.exist?("foo") + assert @cache.exist?("fu") + end + + test "fails with regexp matchers" do + assert_raise ArgumentError do + @cache.delete_matched(/OO/i) + end + end + end +end diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb index 67813a749e..015e17deb9 100644 --- a/activesupport/test/callback_inheritance_test.rb +++ b/activesupport/test/callback_inheritance_test.rb @@ -164,10 +164,10 @@ end class DynamicInheritedCallbacks < ActiveSupport::TestCase def test_callbacks_looks_to_the_superclass_before_running child = EmptyChild.new.dispatch - assert !child.performed? + assert_not_predicate child, :performed? EmptyParent.set_callback :dispatch, :before, :perform! child = EmptyChild.new.dispatch - assert child.performed? + assert_predicate child, :performed? end def test_callbacks_should_be_performed_once_in_child_class diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 5f894db46f..30f1632460 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -77,7 +77,7 @@ module CallbacksTest skip_callback :save, :after, :after_save_method, unless: :yes skip_callback :save, :after, :after_save_method, if: :no skip_callback :save, :before, :before_save_method, unless: :no - skip_callback :save, :before, CallbackClass , if: :yes + skip_callback :save, :before, CallbackClass, if: :yes def yes; true; end def no; false; end end diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb index 7b97028e8c..8cfcaedafd 100644 --- a/activesupport/test/class_cache_test.rb +++ b/activesupport/test/class_cache_test.rb @@ -11,17 +11,17 @@ module ActiveSupport end def test_empty? - assert @cache.empty? + assert_empty @cache @cache.store(ClassCacheTest) - assert !@cache.empty? + assert_not_empty @cache end def test_clear! - assert @cache.empty? + assert_empty @cache @cache.store(ClassCacheTest) - assert !@cache.empty? + assert_not_empty @cache @cache.clear! - assert @cache.empty? + assert_empty @cache end def test_set_key @@ -40,29 +40,29 @@ module ActiveSupport end def test_get_constantizes - assert @cache.empty? + assert_empty @cache assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name) end def test_get_constantizes_fails_on_invalid_names - assert @cache.empty? + assert_empty @cache assert_raise NameError do @cache.get("OmgTotallyInvalidConstantName") end end def test_get_alias - assert @cache.empty? + assert_empty @cache assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name) end def test_safe_get_constantizes - assert @cache.empty? + assert_empty @cache assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name) end def test_safe_get_constantizes_doesnt_fail_on_invalid_names - assert @cache.empty? + assert_empty @cache assert_nil @cache.safe_get("OmgTotallyInvalidConstantName") end diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index ef75a320d1..98d8f3ee0d 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -91,7 +91,7 @@ class ConcernTest < ActiveSupport::TestCase end end @klass.include test_module - assert_equal false, Object.respond_to?(:test) + assert_not_respond_to Object, :test Qux.class_eval do remove_const :ClassMethods end diff --git a/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb new file mode 100644 index 0000000000..2d0f45ec5f --- /dev/null +++ b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "concurrent/atomic/count_down_latch" +require "active_support/concurrency/load_interlock_aware_monitor" + +module ActiveSupport + module Concurrency + class LoadInterlockAwareMonitorTest < ActiveSupport::TestCase + def setup + @monitor = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new + end + + def test_entering_with_no_blocking + assert @monitor.mon_enter + end + + def test_entering_with_blocking + load_interlock_latch = Concurrent::CountDownLatch.new + monitor_latch = Concurrent::CountDownLatch.new + + able_to_use_monitor = false + able_to_load = false + + thread_with_load_interlock = Thread.new do + ActiveSupport::Dependencies.interlock.running do + load_interlock_latch.count_down + monitor_latch.wait + + @monitor.synchronize do + able_to_use_monitor = true + end + end + end + + thread_with_monitor_lock = Thread.new do + @monitor.synchronize do + monitor_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_monitor_lock.join + + assert able_to_use_monitor + assert able_to_load + end + end + end +end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index 10719596df..1cf40261dc 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -43,11 +43,11 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase test "configuration accessors are not available on instance" do instance = Parent.new - assert !instance.respond_to?(:bar) - assert !instance.respond_to?(:bar=) + assert_not_respond_to instance, :bar + assert_not_respond_to instance, :bar= - assert !instance.respond_to?(:baz) - assert !instance.respond_to?(:baz=) + assert_not_respond_to instance, :baz + assert_not_respond_to instance, :baz= end test "configuration accessors can take a default value" do diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb index 0f919efcb0..0a7c43d421 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -90,7 +90,7 @@ class ToXmlTest < ActiveSupport::TestCase def test_to_xml_with_hash_elements xml = [ { name: "David", age: 26, age_in_millis: 820497600000 }, - { name: "Jason", age: 31, age_in_millis: BigDecimal.new("1.0") } + { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") } ].to_xml(skip_instruct: true, indent: 0) assert_equal '<objects type="array"><object>', xml.first(30) @@ -173,7 +173,7 @@ class ToXmlTest < ActiveSupport::TestCase def test_to_xml_with_instruct xml = [ { name: "David", age: 26, age_in_millis: 820497600000 }, - { name: "Jason", age: 31, age_in_millis: BigDecimal.new("1.0") } + { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") } ].to_xml(skip_instruct: false, indent: 0) assert_match(/^<\?xml [^>]*/, xml) @@ -183,7 +183,7 @@ class ToXmlTest < ActiveSupport::TestCase def test_to_xml_with_block xml = [ { name: "David", age: 26, age_in_millis: 820497600000 }, - { name: "Jason", age: 31, age_in_millis: BigDecimal.new("1.0") } + { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") } ].to_xml(skip_instruct: true, indent: 0) do |builder| builder.count 2 end diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb index da9d4963d8..c182b91826 100644 --- a/activesupport/test/core_ext/array/grouping_test.rb +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -116,7 +116,7 @@ class SplitTest < ActiveSupport::TestCase def test_split_with_block a = (1..10).to_a assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 } - assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 , 10], a + assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], a end def test_split_with_edge_values diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index 66e81f1162..62588be33b 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -5,7 +5,7 @@ require "active_support/core_ext/big_decimal" class BigDecimalTest < ActiveSupport::TestCase def test_to_s - bd = BigDecimal.new "0.01" + bd = BigDecimal "0.01" assert_equal "0.01", bd.to_s assert_equal "+0.01", bd.to_s("+F") assert_equal "+0.0 1", bd.to_s("+1F") diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb index 9cc006fc63..5ea288738e 100644 --- a/activesupport/test/core_ext/class_test.rb +++ b/activesupport/test/core_ext/class_test.rb @@ -30,11 +30,11 @@ class ClassTest < ActiveSupport::TestCase def test_descendants_excludes_singleton_classes klass = Parent.new.singleton_class - refute Parent.descendants.include?(klass), "descendants should not include singleton classes" + assert_not Parent.descendants.include?(klass), "descendants should not include singleton classes" end def test_subclasses_excludes_singleton_classes klass = Parent.new.singleton_class - refute Parent.subclasses.include?(klass), "subclasses should not include singleton classes" + assert_not Parent.subclasses.include?(klass), "subclasses should not include singleton classes" end end diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index 42da6f6cd0..1422f135a8 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -297,24 +297,24 @@ module DateAndTimeBehavior def test_beginning_of_week assert_equal date_time_init(2005, 1, 31, 0, 0, 0), date_time_init(2005, 2, 4, 10, 10, 10).beginning_of_week - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 28, 0, 0, 0).beginning_of_week #monday - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 29, 0, 0, 0).beginning_of_week #tuesday - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 30, 0, 0, 0).beginning_of_week #wednesday - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 01, 0, 0, 0).beginning_of_week #thursday - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 02, 0, 0, 0).beginning_of_week #friday - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 03, 0, 0, 0).beginning_of_week #saturday - assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 04, 0, 0, 0).beginning_of_week #sunday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 28, 0, 0, 0).beginning_of_week # monday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 29, 0, 0, 0).beginning_of_week # tuesday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 30, 0, 0, 0).beginning_of_week # wednesday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 01, 0, 0, 0).beginning_of_week # thursday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 02, 0, 0, 0).beginning_of_week # friday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 03, 0, 0, 0).beginning_of_week # saturday + assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 04, 0, 0, 0).beginning_of_week # sunday end def test_end_of_week assert_equal date_time_init(2008, 1, 6, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 31, 10, 10, 10).end_of_week - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 27, 0, 0, 0).end_of_week #monday - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 28, 0, 0, 0).end_of_week #tuesday - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 29, 0, 0, 0).end_of_week #wednesday - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 30, 0, 0, 0).end_of_week #thursday - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 31, 0, 0, 0).end_of_week #friday - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 01, 0, 0, 0).end_of_week #saturday - assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 02, 0, 0, 0).end_of_week #sunday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 27, 0, 0, 0).end_of_week # monday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 28, 0, 0, 0).end_of_week # tuesday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 29, 0, 0, 0).end_of_week # wednesday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 30, 0, 0, 0).end_of_week # thursday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 31, 0, 0, 0).end_of_week # friday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 01, 0, 0, 0).end_of_week # saturday + assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 02, 0, 0, 0).end_of_week # sunday end def test_end_of_month @@ -328,6 +328,26 @@ module DateAndTimeBehavior assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 31, 10, 10, 10).end_of_year end + def test_next_occurring + assert_equal date_time_init(2017, 12, 18, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:monday) + assert_equal date_time_init(2017, 12, 19, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:tuesday) + assert_equal date_time_init(2017, 12, 20, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:wednesday) + assert_equal date_time_init(2017, 12, 21, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:thursday) + assert_equal date_time_init(2017, 12, 15, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:friday) + assert_equal date_time_init(2017, 12, 16, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:saturday) + assert_equal date_time_init(2017, 12, 17, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:sunday) + end + + def test_prev_occurring + assert_equal date_time_init(2017, 12, 11, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:monday) + assert_equal date_time_init(2017, 12, 12, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:tuesday) + assert_equal date_time_init(2017, 12, 13, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:wednesday) + assert_equal date_time_init(2017, 12, 7, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:thursday) + assert_equal date_time_init(2017, 12, 8, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:friday) + assert_equal date_time_init(2017, 12, 9, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:saturday) + assert_equal date_time_init(2017, 12, 10, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:sunday) + end + def test_monday_with_default_beginning_of_week_set with_bw_default(:saturday) do assert_equal date_time_init(2012, 9, 17, 0, 0, 0), date_time_init(2012, 9, 18, 0, 0, 0).monday @@ -341,28 +361,28 @@ module DateAndTimeBehavior end def test_on_weekend_on_saturday - assert date_time_init(2015, 1, 3, 0, 0, 0).on_weekend? - assert date_time_init(2015, 1, 3, 15, 15, 10).on_weekend? + assert_predicate date_time_init(2015, 1, 3, 0, 0, 0), :on_weekend? + assert_predicate date_time_init(2015, 1, 3, 15, 15, 10), :on_weekend? end def test_on_weekend_on_sunday - assert date_time_init(2015, 1, 4, 0, 0, 0).on_weekend? - assert date_time_init(2015, 1, 4, 15, 15, 10).on_weekend? + assert_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekend? + assert_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekend? end def test_on_weekend_on_monday - assert_not date_time_init(2015, 1, 5, 0, 0, 0).on_weekend? - assert_not date_time_init(2015, 1, 5, 15, 15, 10).on_weekend? + assert_not_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekend? + assert_not_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekend? end def test_on_weekday_on_sunday - assert_not date_time_init(2015, 1, 4, 0, 0, 0).on_weekday? - assert_not date_time_init(2015, 1, 4, 15, 15, 10).on_weekday? + assert_not_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekday? + assert_not_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekday? end def test_on_weekday_on_monday - assert date_time_init(2015, 1, 5, 0, 0, 0).on_weekday? - assert date_time_init(2015, 1, 5, 15, 15, 10).on_weekday? + assert_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekday? + assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday? end def with_bw_default(bw = :monday) diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 0c6f3f595a..b8652884ce 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -95,11 +95,11 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end def test_beginning_of_week_in_calendar_reform - assert_equal Date.new(1582, 10, 1), Date.new(1582, 10, 15).beginning_of_week #friday + assert_equal Date.new(1582, 10, 1), Date.new(1582, 10, 15).beginning_of_week # friday end def test_end_of_week_in_calendar_reform - assert_equal Date.new(1582, 10, 17), Date.new(1582, 10, 4).end_of_week #thursday + assert_equal Date.new(1582, 10, 17), Date.new(1582, 10, 4).end_of_week # thursday end def test_end_of_year @@ -144,7 +144,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase assert_equal Date.new(2012, 9, 28), Date.new(2005, 2, 28).advance(years: 7, months: 7) assert_equal Date.new(2013, 10, 3), Date.new(2005, 2, 28).advance(years: 7, months: 19, days: 5) assert_equal Date.new(2013, 10, 17), Date.new(2005, 2, 28).advance(years: 7, months: 19, weeks: 2, days: 5) - assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29).advance(years: 1) #leap day plus one year + assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29).advance(years: 1) # leap day plus one year end def test_advance_does_first_years_and_then_days @@ -386,11 +386,11 @@ end class DateExtBehaviorTest < ActiveSupport::TestCase def test_date_acts_like_date - assert Date.new.acts_like_date? + assert_predicate Date.new, :acts_like_date? end def test_blank? - assert_not Date.new.blank? + assert_not_predicate Date.new, :blank? end def test_freeze_doesnt_clobber_memoized_instance_methods diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index d942cddb2a..894fb80cba 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -30,28 +30,6 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end end - def test_next_occur - datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday - assert_equal datetime.next_occurring(:monday), datetime.since(2.days) - assert_equal datetime.next_occurring(:tuesday), datetime.since(3.days) - assert_equal datetime.next_occurring(:wednesday), datetime.since(4.days) - assert_equal datetime.next_occurring(:thursday), datetime.since(5.days) - assert_equal datetime.next_occurring(:friday), datetime.since(6.days) - assert_equal datetime.next_occurring(:saturday), datetime.since(1.week) - assert_equal datetime.next_occurring(:sunday), datetime.since(1.day) - end - - def test_prev_occur - datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday - assert_equal datetime.prev_occurring(:monday), datetime.ago(5.days) - assert_equal datetime.prev_occurring(:tuesday), datetime.ago(4.days) - assert_equal datetime.prev_occurring(:wednesday), datetime.ago(3.days) - assert_equal datetime.prev_occurring(:thursday), datetime.ago(2.days) - assert_equal datetime.prev_occurring(:friday), datetime.ago(1.day) - assert_equal datetime.prev_occurring(:saturday), datetime.ago(1.week) - assert_equal datetime.prev_occurring(:sunday), datetime.ago(6.days) - end - def test_readable_inspect datetime = DateTime.new(2005, 2, 21, 14, 30, 0) assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.readable_inspect @@ -209,7 +187,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2013, 10, 3, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, days: 5) assert_equal DateTime.civil(2013, 10, 17, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5) assert_equal DateTime.civil(2001, 12, 27, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1) - assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10).advance(years: 1) #leap day plus one year + assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year assert_equal DateTime.civil(2005, 2, 28, 20, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: 5) assert_equal DateTime.civil(2005, 2, 28, 15, 22, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(minutes: 7) assert_equal DateTime.civil(2005, 2, 28, 15, 15, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(seconds: 9) @@ -337,15 +315,15 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_acts_like_date - assert DateTime.new.acts_like_date? + assert_predicate DateTime.new, :acts_like_date? end def test_acts_like_time - assert DateTime.new.acts_like_time? + assert_predicate DateTime.new, :acts_like_time? end def test_blank? - assert_not DateTime.new.blank? + assert_not_predicate DateTime.new, :blank? end def test_utc? @@ -385,45 +363,45 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_compare_with_time - assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59) - assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) + assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59) + assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1)) end def test_compare_with_datetime - assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) - assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) + assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) + assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) assert_equal(-1, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1)) end def test_compare_with_time_with_zone - assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) - assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) + assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) + assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) assert_equal(-1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"])) end def test_compare_with_string - assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s - assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s + assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s + assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1).to_s) assert_nil DateTime.civil(2000) <=> "Invalid as Time" end def test_compare_with_integer - assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587 - assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588 + assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587 + assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588 assert_equal(-1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440589) end def test_compare_with_float - assert_equal 1, DateTime.civil(1970) <=> 2440586.5 - assert_equal 0, DateTime.civil(1970) <=> 2440587.5 + assert_equal 1, DateTime.civil(1970) <=> 2440586.5 + assert_equal 0, DateTime.civil(1970) <=> 2440587.5 assert_equal(-1, DateTime.civil(1970) <=> 2440588.5) end def test_compare_with_rational - assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2) - assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2) + assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2) + assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2) assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2)) end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index b3e0cd8bd0..c5022cf5b1 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -5,6 +5,7 @@ require "active_support/inflector" require "active_support/time" require "active_support/json" require "time_zone_test_helpers" +require "yaml" class DurationTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -71,6 +72,8 @@ class DurationTest < ActiveSupport::TestCase assert_equal "7 days", 7.days.inspect assert_equal "1 week", 1.week.inspect assert_equal "2 weeks", 1.fortnight.inspect + assert_equal "0 seconds", (10 % 5.seconds).inspect + assert_equal "10 minutes", (10.minutes + 0.seconds).inspect end def test_inspect_locale @@ -160,7 +163,7 @@ class DurationTest < ActiveSupport::TestCase end def test_time_plus_duration_returns_same_time_datatype - twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"] , Time.utc(2016, 4, 28, 00, 45)) + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"], Time.utc(2016, 4, 28, 00, 45)) now = Time.now.utc %w( second minute hour day week month year ).each do |unit| assert_equal((now + 1.send(unit)).class, Time, "Time + 1.#{unit} must be Time") @@ -640,6 +643,12 @@ class DurationTest < ActiveSupport::TestCase assert_equal time + d1, time + d2 end + def test_durations_survive_yaml_serialization + d1 = YAML.load(YAML.dump(10.minutes)) + assert_equal 600, d1.to_i + assert_equal 660, (d1 + 60).to_i + end + private def eastern_time_zone if Gem.win_platform? diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 746d7ad416..50b811e79f 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -446,7 +446,7 @@ class HashExtTest < ActiveSupport::TestCase original.freeze assert_nothing_raised { original.except(:a) } - assert_raise(RuntimeError) { original.except!(:a) } + assert_raise(frozen_error_class) { original.except!(:a) } end def test_except_does_not_delete_values_in_original @@ -1061,7 +1061,7 @@ class HashToXmlTest < ActiveSupport::TestCase </alert> XML alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"] - assert alert_at.utc? + assert_predicate alert_at, :utc? assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at end @@ -1072,7 +1072,7 @@ class HashToXmlTest < ActiveSupport::TestCase </alert> XML alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"] - assert alert_at.utc? + assert_predicate alert_at, :utc? assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at end @@ -1083,7 +1083,7 @@ class HashToXmlTest < ActiveSupport::TestCase </alert> XML alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"] - assert alert_at.utc? + assert_predicate alert_at, :utc? assert_equal 2050, alert_at.year assert_equal 2, alert_at.month assert_equal 10, alert_at.day diff --git a/activesupport/test/core_ext/module/anonymous_test.rb b/activesupport/test/core_ext/module/anonymous_test.rb index 606f22c9b5..e03c217015 100644 --- a/activesupport/test/core_ext/module/anonymous_test.rb +++ b/activesupport/test/core_ext/module/anonymous_test.rb @@ -5,12 +5,12 @@ require "active_support/core_ext/module/anonymous" class AnonymousTest < ActiveSupport::TestCase test "an anonymous class or module are anonymous" do - assert Module.new.anonymous? - assert Class.new.anonymous? + assert_predicate Module.new, :anonymous? + assert_predicate Class.new, :anonymous? end test "a named class or module are not anonymous" do - assert !Kernel.anonymous? - assert !Object.anonymous? + assert_not_predicate Kernel, :anonymous? + assert_not_predicate Object, :anonymous? end end diff --git a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb index e0fbd1002c..e0e331fc91 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb @@ -43,22 +43,22 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase assert_respond_to @class, :foo assert_respond_to @class, :foo= assert_respond_to @object, :bar - assert !@object.respond_to?(:bar=) + assert_not_respond_to @object, :bar= end.join end def test_should_not_create_instance_reader Thread.new do assert_respond_to @class, :shaq - assert !@object.respond_to?(:shaq) + assert_not_respond_to @object, :shaq end.join end def test_should_not_create_instance_accessors Thread.new do assert_respond_to @class, :camp - assert !@object.respond_to?(:camp) - assert !@object.respond_to?(:camp=) + assert_not_respond_to @object, :camp + assert_not_respond_to @object, :camp= end.join end diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index f1d6859a88..33c583947a 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -65,18 +65,18 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_respond_to @module, :foo assert_respond_to @module, :foo= assert_respond_to @object, :bar - assert !@object.respond_to?(:bar=) + assert_not_respond_to @object, :bar= end def test_should_not_create_instance_reader assert_respond_to @module, :shaq - assert !@object.respond_to?(:shaq) + assert_not_respond_to @object, :shaq end def test_should_not_create_instance_accessors assert_respond_to @module, :camp - assert !@object.respond_to?(:camp) - assert !@object.respond_to?(:camp=) + assert_not_respond_to @object, :camp + assert_not_respond_to @object, :camp= end def test_should_raise_name_error_if_attribute_name_is_invalid diff --git a/activesupport/test/core_ext/module/attribute_aliasing_test.rb b/activesupport/test/core_ext/module/attribute_aliasing_test.rb index 187a0f4da2..81aac224f9 100644 --- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb +++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb @@ -30,15 +30,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase def test_attribute_alias e = AttributeAliasing::Email.new - assert !e.subject? + assert_not_predicate e, :subject? e.title = "Upgrade computer" assert_equal "Upgrade computer", e.subject - assert e.subject? + assert_predicate e, :subject? e.subject = "We got a long way to go" assert_equal "We got a long way to go", e.title - assert e.title? + assert_predicate e, :title? end def test_aliasing_to_uppercase_attributes @@ -47,15 +47,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase # to more sensible ones, everything goes *foof*. e = AttributeAliasing::Email.new - assert !e.body? - assert !e.Data? + assert_not_predicate e, :body? + assert_not_predicate e, :Data? e.body = "No, really, this is not a joke." assert_equal "No, really, this is not a joke.", e.Data - assert e.Data? + assert_predicate e, :Data? e.Data = "Uppercased methods are the suck" assert_equal "Uppercased methods are the suck", e.body - assert e.body? + assert_predicate e, :body? end end diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb index 192c3d5a9c..969434766f 100644 --- a/activesupport/test/core_ext/module/concerning_test.rb +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -55,10 +55,10 @@ class ModuleConcernTest < ActiveSupport::TestCase end def test_using_class_methods_blocks_instead_of_ClassMethods_module - assert !Foo.respond_to?(:will_be_orphaned) - assert Foo.respond_to?(:hacked_on) - assert Foo.respond_to?(:nicer_dsl) - assert Foo.respond_to?(:doesnt_clobber) + assert_not_respond_to Foo, :will_be_orphaned + assert_respond_to Foo, :hacked_on + assert_respond_to Foo, :nicer_dsl + assert_respond_to Foo, :doesnt_clobber # Orphan in Foo::ClassMethods, not Bar::ClassMethods. assert Foo.const_defined?(:ClassMethods) diff --git a/activesupport/test/core_ext/module/reachable_test.rb b/activesupport/test/core_ext/module/reachable_test.rb index 097a72fa5b..f356d46957 100644 --- a/activesupport/test/core_ext/module/reachable_test.rb +++ b/activesupport/test/core_ext/module/reachable_test.rb @@ -6,15 +6,15 @@ require "active_support/core_ext/module/reachable" class AnonymousTest < ActiveSupport::TestCase test "an anonymous class or module is not reachable" do assert_deprecated do - assert !Module.new.reachable? - assert !Class.new.reachable? + assert_not_predicate Module.new, :reachable? + assert_not_predicate Class.new, :reachable? end end test "ordinary named classes or modules are reachable" do assert_deprecated do - assert Kernel.reachable? - assert Object.reachable? + assert_predicate Kernel, :reachable? + assert_predicate Object, :reachable? end end @@ -26,8 +26,8 @@ class AnonymousTest < ActiveSupport::TestCase self.class.send(:remove_const, :M) assert_deprecated do - assert !c.reachable? - assert !m.reachable? + assert_not_predicate c, :reachable? + assert_not_predicate m, :reachable? end end @@ -42,10 +42,10 @@ class AnonymousTest < ActiveSupport::TestCase eval "module M; end" assert_deprecated do - assert C.reachable? - assert M.reachable? - assert !c.reachable? - assert !m.reachable? + assert_predicate C, :reachable? + assert_predicate M, :reachable? + assert_not_predicate c, :reachable? + assert_not_predicate m, :reachable? end end end diff --git a/activesupport/test/core_ext/module/remove_method_test.rb b/activesupport/test/core_ext/module/remove_method_test.rb index 8493be8d08..a18fc0a5e4 100644 --- a/activesupport/test/core_ext/module/remove_method_test.rb +++ b/activesupport/test/core_ext/module/remove_method_test.rb @@ -32,14 +32,14 @@ class RemoveMethodTest < ActiveSupport::TestCase RemoveMethodTests::A.class_eval { remove_possible_method(:do_something) } - assert !RemoveMethodTests::A.new.respond_to?(:do_something) + assert_not_respond_to RemoveMethodTests::A.new, :do_something end def test_remove_singleton_method_from_an_object RemoveMethodTests::A.class_eval { remove_possible_singleton_method(:do_something_else) } - assert !RemoveMethodTests::A.respond_to?(:do_something_else) + assert_not_respond_to RemoveMethodTests::A, :do_something_else end def test_redefine_method_in_an_object diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index e918823074..ebbe9c304c 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -347,7 +347,7 @@ class ModuleTest < ActiveSupport::TestCase def test_delegation_with_method_arguments has_block = HasBlock.new(Block.new) - assert has_block.hello? + assert_predicate has_block, :hello? end def test_delegate_missing_to_with_method @@ -383,9 +383,9 @@ class ModuleTest < ActiveSupport::TestCase end def test_delegate_missing_to_affects_respond_to - assert DecoratedTester.new(@david).respond_to?(:name) - assert_not DecoratedTester.new(@david).respond_to?(:private_name) - assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method) + assert_respond_to DecoratedTester.new(@david), :name + assert_not_respond_to DecoratedTester.new(@david), :private_name + assert_not_respond_to DecoratedTester.new(@david), :my_fake_method assert DecoratedTester.new(@david).respond_to?(:name, true) assert_not DecoratedTester.new(@david).respond_to?(:private_name, true) @@ -414,8 +414,8 @@ class ModuleTest < ActiveSupport::TestCase place = location.new(Somewhere.new("Such street", "Sad city")) - assert_not place.respond_to?(:street) - assert_not place.respond_to?(:city) + assert_not_respond_to place, :street + assert_not_respond_to place, :city assert place.respond_to?(:street, true) # Asking for private method assert place.respond_to?(:city, true) @@ -432,12 +432,12 @@ class ModuleTest < ActiveSupport::TestCase place = location.new(Somewhere.new("Such street", "Sad city")) - assert_not place.respond_to?(:street) - assert_not place.respond_to?(:city) + assert_not_respond_to place, :street + assert_not_respond_to place, :city - assert_not place.respond_to?(:the_street) + assert_not_respond_to place, :the_street assert place.respond_to?(:the_street, true) - assert_not place.respond_to?(:the_city) + assert_not_respond_to place, :the_city assert place.respond_to?(:the_city, true) end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index d38124b214..4b9073da54 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -302,7 +302,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "40 KB", 41100.to_s(:human_size, precision: 2) assert_equal "1.0 KB", kilobytes(1.0123).to_s(:human_size, precision: 2, strip_insignificant_zeros: false) assert_equal "1.012 KB", kilobytes(1.0123).to_s(:human_size, precision: 3, significant: false) - assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 0, significant: true) #ignores significant it precision is 0 + assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 0, significant: true) # ignores significant it precision is 0 end def test_to_s__human_size_with_custom_delimiter_and_separator @@ -330,17 +330,17 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "489.0 Thousand", 489000.to_s(:human, precision: 4, strip_insignificant_zeros: false) assert_equal "1.2346 Million", 1234567.to_s(:human, precision: 4, significant: false) assert_equal "1,2 Million", 1234567.to_s(:human, precision: 1, significant: false, separator: ",") - assert_equal "1 Million", 1234567.to_s(:human, precision: 0, significant: true, separator: ",") #significant forced to false + assert_equal "1 Million", 1234567.to_s(:human, precision: 0, significant: true, separator: ",") # significant forced to false end def test_number_to_human_with_custom_units - #Only integers + # Only integers volume = { unit: "ml", thousand: "lt", million: "m3" } assert_equal "123 lt", 123456.to_s(:human, units: volume) assert_equal "12 ml", 12.to_s(:human, units: volume) assert_equal "1.23 m3", 1234567.to_s(:human, units: volume) - #Including fractionals + # Including fractionals distance = { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" } assert_equal "1.23 mm", 0.00123.to_s(:human, units: distance) assert_equal "1.23 cm", 0.0123.to_s(:human, units: distance) @@ -353,14 +353,14 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "1.23 km", 1230.to_s(:human, units: distance) assert_equal "12.3 km", 12300.to_s(:human, units: distance) - #The quantifiers don't need to be a continuous sequence + # The quantifiers don't need to be a continuous sequence gangster = { hundred: "hundred bucks", million: "thousand quids" } assert_equal "1 hundred bucks", 100.to_s(:human, units: gangster) assert_equal "25 hundred bucks", 2500.to_s(:human, units: gangster) assert_equal "25 thousand quids", 25000000.to_s(:human, units: gangster) assert_equal "12300 thousand quids", 12345000000.to_s(:human, units: gangster) - #Spaces are stripped from the resulting string + # Spaces are stripped from the resulting string assert_equal "4", 4.to_s(:human, units: { unit: "", ten: "tens " }) assert_equal "4.5 tens", 45.to_s(:human, units: { unit: "", ten: " tens " }) end diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 749e59ec00..954f415383 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -16,8 +16,8 @@ class BlankTest < ActiveSupport::TestCase end end - BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ] - NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now ] + BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {}, " ".encode("UTF-16LE") ] + NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now, "my value".encode("UTF-16LE") ] def test_blank BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" } diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index 93f39b00bb..b984becce3 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -8,16 +8,16 @@ require "active_support/core_ext/numeric/time" class DuplicableTest < ActiveSupport::TestCase if RUBY_VERSION >= "2.5.0" RAISE_DUP = [method(:puts)] - ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)] + ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)] elsif RUBY_VERSION >= "2.4.1" RAISE_DUP = [method(:puts), Complex(1), Rational(1)] - ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3] + ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3] elsif RUBY_VERSION >= "2.4.0" # Due to 2.4.0 bug. This elsif cannot be removed unless we drop 2.4.0 support... RAISE_DUP = [method(:puts), Complex(1), Rational(1), "symbol_from_string".to_sym] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3] + ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3] else RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts), Complex(1), Rational(1)] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56")] + ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56")] end def test_duplicable diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb index b9ec827954..a3d8daab5b 100644 --- a/activesupport/test/core_ext/object/instance_variables_test.rb +++ b/activesupport/test/core_ext/object/instance_variables_test.rb @@ -19,7 +19,7 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase end def test_instance_exec_passes_arguments_to_block - assert_equal %w(hello goodbye), "hello".instance_exec("goodbye") { |v| [self, v] } + assert_equal %w(hello goodbye), "hello".dup.instance_exec("goodbye") { |v| [self, v] } end def test_instance_exec_with_frozen_obj @@ -27,7 +27,7 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase end def test_instance_exec_nested - assert_equal %w(goodbye olleh bar), "hello".instance_exec("goodbye") { |arg| + assert_equal %w(goodbye olleh bar), "hello".dup.instance_exec("goodbye") { |arg| [arg] + instance_exec("bar") { |v| [reverse, v] } } end end diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index fe68b24bf5..40d6cdd28e 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -10,25 +10,25 @@ class ObjectTryTest < ActiveSupport::TestCase def test_nonexisting_method method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_nil @string.try(method) end def test_nonexisting_method_with_arguments method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_nil @string.try(method, "llo", "y") end def test_nonexisting_method_bang method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_raise(NoMethodError) { @string.try!(method) } end def test_nonexisting_method_with_arguments_bang method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_raise(NoMethodError) { @string.try!(method, "llo", "y") } end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 049fac8fd4..903c173e59 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -106,26 +106,26 @@ class RangeTest < ActiveSupport::TestCase end 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)) + 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 {} 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)) + 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) {} end end def test_include_on_time_with_zone - twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"] , Time.utc(2006, 11, 28, 10, 30)) + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30)) assert ((twz - 1.hour)..twz).include?(twz) end def test_case_equals_on_time_with_zone - twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"] , Time.utc(2006, 11, 28, 10, 30)) + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30)) assert ((twz - 1.hour)..twz) === twz end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 5c5abe9fd1..472190277e 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -259,8 +259,8 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_string_inquiry - assert "production".inquiry.production? - assert !"production".inquiry.development? + assert_predicate "production".inquiry, :production? + assert_not_predicate "production".inquiry, :development? end def test_truncate @@ -317,7 +317,7 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_truncate_should_not_be_html_safe - assert !"Hello World!".truncate(12).html_safe? + assert_not_predicate "Hello World!".truncate(12), :html_safe? end def test_remove @@ -639,7 +639,7 @@ end class StringBehaviourTest < ActiveSupport::TestCase def test_acts_like_string - assert "Bambi".acts_like_string? + assert_predicate "Bambi", :acts_like_string? end end @@ -654,10 +654,10 @@ class CoreExtStringMultibyteTest < ActiveSupport::TestCase end def test_string_should_recognize_utf8_strings - assert UTF8_STRING.is_utf8? - assert ASCII_STRING.is_utf8? - assert !EUC_JP_STRING.is_utf8? - assert !INVALID_UTF8_STRING.is_utf8? + assert_predicate UTF8_STRING, :is_utf8? + assert_predicate ASCII_STRING, :is_utf8? + assert_not_predicate EUC_JP_STRING, :is_utf8? + assert_not_predicate INVALID_UTF8_STRING, :is_utf8? end def test_mb_chars_returns_instance_of_proxy_class @@ -676,12 +676,12 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "A string is unsafe by default" do - assert !@string.html_safe? + assert_not_predicate @string, :html_safe? end test "A string can be marked safe" do string = @string.html_safe - assert string.html_safe? + assert_predicate string, :html_safe? end test "Marking a string safe returns the string" do @@ -689,15 +689,15 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "An integer is safe by default" do - assert 5.html_safe? + assert_predicate 5, :html_safe? end test "a float is safe by default" do - assert 5.7.html_safe? + assert_predicate 5.7, :html_safe? end test "An object is unsafe by default" do - assert !@object.html_safe? + assert_not_predicate @object, :html_safe? end test "Adding an object to a safe string returns a safe string" do @@ -705,7 +705,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string << @object assert_equal "helloother", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Adding a safe string to another safe string returns a safe string" do @@ -714,7 +714,7 @@ class OutputSafetyTest < ActiveSupport::TestCase @combination = @other_string + string assert_equal "otherhello", @combination - assert @combination.html_safe? + assert_predicate @combination, :html_safe? end test "Adding an unsafe string to a safe string escapes it and returns a safe string" do @@ -725,20 +725,20 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal "other<foo>", @combination assert_equal "hello<foo>", @other_combination - assert @combination.html_safe? - assert !@other_combination.html_safe? + assert_predicate @combination, :html_safe? + assert_not_predicate @other_combination, :html_safe? end test "Prepending safe onto unsafe yields unsafe" do @string.prepend "other".html_safe - assert !@string.html_safe? + assert_not_predicate @string, :html_safe? assert_equal "otherhello", @string end test "Prepending unsafe onto safe yields escaped safe" do other = "other".html_safe other.prepend "<foo>" - assert other.html_safe? + assert_predicate other, :html_safe? assert_equal "<foo>other", other end @@ -747,14 +747,14 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string.concat(string) - assert !@other_string.html_safe? + assert_not_predicate @other_string, :html_safe? end test "Concatting unsafe onto safe yields escaped safe" do @other_string = "other".html_safe string = @other_string.concat("<foo>") assert_equal "other<foo>", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Concatting safe onto safe yields safe" do @@ -762,7 +762,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string.concat(string) - assert @other_string.html_safe? + assert_predicate @other_string, :html_safe? end test "Concatting safe onto unsafe with << yields unsafe" do @@ -770,14 +770,14 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string << string - assert !@other_string.html_safe? + assert_not_predicate @other_string, :html_safe? end test "Concatting unsafe onto safe with << yields escaped safe" do @other_string = "other".html_safe string = @other_string << "<foo>" assert_equal "other<foo>", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Concatting safe onto safe with << yields safe" do @@ -785,7 +785,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string << string - assert @other_string.html_safe? + assert_predicate @other_string, :html_safe? end test "Concatting safe onto unsafe with % yields unsafe" do @@ -793,7 +793,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string = @other_string % string - assert !@other_string.html_safe? + assert_not_predicate @other_string, :html_safe? end test "Concatting unsafe onto safe with % yields escaped safe" do @@ -801,7 +801,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @other_string % "<foo>" assert_equal "other<foo>", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Concatting safe onto safe with % yields safe" do @@ -809,7 +809,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string = @other_string % string - assert @other_string.html_safe? + assert_predicate @other_string, :html_safe? end test "Concatting with % doesn't modify a string" do @@ -823,7 +823,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe string = string.concat(13) assert_equal "hello".dup.concat(13), string - assert string.html_safe? + assert_predicate string, :html_safe? end test "emits normal string yaml" do @@ -832,8 +832,8 @@ class OutputSafetyTest < ActiveSupport::TestCase test "call to_param returns a normal string" do string = @string.html_safe - assert string.html_safe? - assert !string.to_param.html_safe? + assert_predicate string, :html_safe? + assert_not_predicate string.to_param, :html_safe? end test "ERB::Util.html_escape should escape unsafe characters" do diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 8cb17df01b..e1cb22fda8 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -451,7 +451,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2013, 10, 3, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, days: 5) assert_equal Time.local(2013, 10, 17, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5) assert_equal Time.local(2001, 12, 27, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1) - assert_equal Time.local(2005, 2, 28, 15, 15, 10), Time.local(2004, 2, 29, 15, 15, 10).advance(years: 1) #leap day plus one year + assert_equal Time.local(2005, 2, 28, 15, 15, 10), Time.local(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year assert_equal Time.local(2005, 2, 28, 20, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: 5) assert_equal Time.local(2005, 2, 28, 15, 22, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(minutes: 7) assert_equal Time.local(2005, 2, 28, 15, 15, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(seconds: 9) @@ -473,7 +473,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.utc(2013, 10, 3, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 7, months: 19, days: 11) assert_equal Time.utc(2013, 10, 17, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5) assert_equal Time.utc(2001, 12, 27, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1) - assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10).advance(years: 1) #leap day plus one year + assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year assert_equal Time.utc(2005, 2, 28, 20, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: 5) assert_equal Time.utc(2005, 2, 28, 15, 22, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(minutes: 7) assert_equal Time.utc(2005, 2, 28, 15, 15, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(seconds: 9) @@ -495,7 +495,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.new(2013, 10, 3, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 7, months: 19, days: 11) assert_equal Time.new(2013, 10, 17, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: 7, months: 19, weeks: 2, days: 5) assert_equal Time.new(2001, 12, 27, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: -3, months: -2, days: -1) - assert_equal Time.new(2005, 2, 28, 15, 15, 10, "-08:00"), Time.new(2004, 2, 29, 15, 15, 10, "-08:00").advance(years: 1) #leap day plus one year + assert_equal Time.new(2005, 2, 28, 15, 15, 10, "-08:00"), Time.new(2004, 2, 29, 15, 15, 10, "-08:00").advance(years: 1) # leap day plus one year assert_equal Time.new(2005, 2, 28, 20, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: 5) assert_equal Time.new(2005, 2, 28, 15, 22, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(minutes: 7) assert_equal Time.new(2005, 2, 28, 15, 15, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(seconds: 9) @@ -737,7 +737,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_acts_like_time - assert Time.new.acts_like_time? + assert_predicate Time.new, :acts_like_time? end def test_formatted_offset_with_utc @@ -756,26 +756,26 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_compare_with_time - assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999) - assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) + assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999) + assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0, 001)) end def test_compare_with_datetime - assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) - assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) + assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) + assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) assert_equal(-1, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1)) end def test_compare_with_time_with_zone - assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) - assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) + assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) + assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"])) end def test_compare_with_string - assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s - assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s + assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s + assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1, 0).to_s) assert_nil Time.utc(2000) <=> "Invalid as Time" end @@ -863,11 +863,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_minus_with_time_with_zone - assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"]) + assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"]) end def test_minus_with_datetime - assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1) + assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1) end def test_time_created_with_local_constructor_cannot_represent_times_during_hour_skipped_by_dst @@ -875,7 +875,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase # On Apr 2 2006 at 2:00AM in US, clocks were moved forward to 3:00AM. # Therefore, 2AM EST doesn't exist for this date; Time.local fails over to 3:00AM EDT assert_equal Time.local(2006, 4, 2, 3), Time.local(2006, 4, 2, 2) - assert Time.local(2006, 4, 2, 2).dst? + assert_predicate Time.local(2006, 4, 2, 2), :dst? end end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 0f80a24758..be3b8a49a9 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -50,6 +50,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_raise(ArgumentError) { @twz.in_time_zone(Object.new) } end + def test_in_time_zone_with_ambiguous_time + with_env_tz "Europe/Moscow" do + assert_equal Time.utc(2014, 10, 25, 22, 0, 0), Time.local(2014, 10, 26, 1, 0, 0).in_time_zone("Moscow") + end + end + def test_localtime assert_equal @twz.localtime, @twz.utc.getlocal assert_instance_of Time, @twz.localtime @@ -78,7 +84,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_formatted_offset assert_equal "-05:00", @twz.formatted_offset - assert_equal "-04:00", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).formatted_offset #dst + assert_equal "-04:00", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).formatted_offset # dst end def test_dst? @@ -88,7 +94,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_zone assert_equal "EST", @twz.zone - assert_equal "EDT", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst + assert_equal "EDT", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone # dst end def test_nsec @@ -215,20 +221,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_compare_with_time - assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59) - assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0) + assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59) + assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0) assert_equal(-1, @twz <=> Time.utc(2000, 1, 1, 0, 0, 1)) end def test_compare_with_datetime - assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59) - assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0) + assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59) + assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0) assert_equal(-1, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 1)) end def test_compare_with_time_with_zone - assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) - assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) + assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) + assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) assert_equal(-1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"])) end @@ -301,13 +307,13 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_plus_with_integer - assert_equal Time.utc(1999, 12, 31, 19, 0 , 5), (@twz + 5).time + assert_equal Time.utc(1999, 12, 31, 19, 0, 5), (@twz + 5).time end def test_plus_with_integer_when_self_wraps_datetime datetime = DateTime.civil(2000, 1, 1, 0) twz = ActiveSupport::TimeWithZone.new(datetime, @time_zone) - assert_equal DateTime.civil(1999, 12, 31, 19, 0 , 5), (twz + 5).time + assert_equal DateTime.civil(1999, 12, 31, 19, 0, 5), (twz + 5).time end def test_plus_when_crossing_time_class_limit @@ -316,31 +322,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_plus_with_duration - assert_equal Time.utc(2000, 1, 5, 19, 0 , 0), (@twz + 5.days).time + assert_equal Time.utc(2000, 1, 5, 19, 0, 0), (@twz + 5.days).time end def test_minus_with_integer - assert_equal Time.utc(1999, 12, 31, 18, 59 , 55), (@twz - 5).time + assert_equal Time.utc(1999, 12, 31, 18, 59, 55), (@twz - 5).time end def test_minus_with_integer_when_self_wraps_datetime datetime = DateTime.civil(2000, 1, 1, 0) twz = ActiveSupport::TimeWithZone.new(datetime, @time_zone) - assert_equal DateTime.civil(1999, 12, 31, 18, 59 , 55), (twz - 5).time + assert_equal DateTime.civil(1999, 12, 31, 18, 59, 55), (twz - 5).time end def test_minus_with_duration - assert_equal Time.utc(1999, 12, 26, 19, 0 , 0), (@twz - 5.days).time + assert_equal Time.utc(1999, 12, 26, 19, 0, 0), (@twz - 5.days).time end def test_minus_with_time - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1) end def test_minus_with_time_precision - assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) - assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) end def test_minus_with_time_with_zone @@ -352,20 +358,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_minus_with_time_with_zone_precision twz1 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone["UTC"]) twz2 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - assert_equal 86_399.999999998, twz2 - twz1 + assert_equal 86_399.999999998, twz2 - twz1 end def test_minus_with_datetime - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) end def test_minus_with_datetime_precision - assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) + assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) end def test_minus_with_wrapped_datetime - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) end def test_plus_and_minus_enforce_spring_dst_rules @@ -475,7 +481,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_acts_like_time - assert @twz.acts_like_time? + assert_predicate @twz, :acts_like_time? assert @twz.acts_like?(:time) assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time) end @@ -486,7 +492,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_blank? - assert_not @twz.blank? + assert_not_predicate @twz, :blank? end def test_is_a @@ -501,17 +507,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_method_missing_with_time_return_value assert_instance_of ActiveSupport::TimeWithZone, @twz.months_since(1) - assert_equal Time.utc(2000, 1, 31, 19, 0 , 0), @twz.months_since(1).time + assert_equal Time.utc(2000, 1, 31, 19, 0, 0), @twz.months_since(1).time end def test_marshal_dump_and_load marshal_str = Marshal.dump(@twz) mtime = Marshal.load(marshal_str) assert_equal Time.utc(2000, 1, 1, 0), mtime.utc - assert mtime.utc.utc? + assert_predicate mtime.utc, :utc? assert_equal ActiveSupport::TimeZone["Eastern Time (US & Canada)"], mtime.time_zone assert_equal Time.utc(1999, 12, 31, 19), mtime.time - assert mtime.time.utc? + assert_predicate mtime.time, :utc? assert_equal @twz.inspect, mtime.inspect end @@ -520,16 +526,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase marshal_str = Marshal.dump(twz) mtime = Marshal.load(marshal_str) assert_equal Time.utc(2000, 1, 1, 0), mtime.utc - assert mtime.utc.utc? + assert_predicate mtime.utc, :utc? assert_equal "America/New_York", mtime.time_zone.name assert_equal Time.utc(1999, 12, 31, 19), mtime.time - assert mtime.time.utc? + assert_predicate mtime.time, :utc? assert_equal @twz.inspect, mtime.inspect end def test_freeze @twz.freeze - assert @twz.frozen? + assert_predicate @twz, :frozen? end def test_freeze_preloads_instance_variables @@ -1029,8 +1035,8 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_nil_time_zone Time.use_zone nil do - assert !@t.in_time_zone.respond_to?(:period), "no period method" - assert !@dt.in_time_zone.respond_to?(:period), "no period method" + assert_not_respond_to @t.in_time_zone, :period, "no period method" + assert_not_respond_to @dt.in_time_zone, :period, "no period method" end end @@ -1218,7 +1224,7 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase def test_nil_time_zone with_tz_default nil do - assert !@d.in_time_zone.respond_to?(:period), "no period method" + assert_not_respond_to @d.in_time_zone, :period, "no period method" end end @@ -1267,9 +1273,9 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase def test_nil_time_zone with_tz_default nil do - assert !@s.in_time_zone.respond_to?(:period), "no period method" - assert !@u.in_time_zone.respond_to?(:period), "no period method" - assert !@z.in_time_zone.respond_to?(:period), "no period method" + assert_not_respond_to @s.in_time_zone, :period, "no period method" + assert_not_respond_to @u.in_time_zone, :period, "no period method" + assert_not_respond_to @z.in_time_zone, :period, "no period method" end end @@ -1301,4 +1307,10 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase assert_raise(ArgumentError) { @u.in_time_zone(Object.new) } assert_raise(ArgumentError) { @z.in_time_zone(Object.new) } end + + def test_in_time_zone_with_ambiguous_time + with_tz_default "Moscow" do + assert_equal Time.utc(2014, 10, 25, 22, 0, 0), "2014-10-26 01:00:00".in_time_zone + end + end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index d636da46d2..c0c4c4cac5 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -185,6 +185,61 @@ class DependenciesTest < ActiveSupport::TestCase end end + # Regression see https://github.com/rails/rails/issues/31694 + def test_included_constant_that_changes_to_have_exception_then_back_does_not_loop_forever + # This constant references a nested constant whose namespace will be auto-generated + parent_constant = <<-RUBY + class ConstantReloadError + AnotherConstant::ReloadError + end + RUBY + + # This constant's namespace will be auto-generated, + # also, we'll edit it to contain an error at load-time + child_constant = <<-RUBY + class AnotherConstant::ReloadError + # no_such_method_as_this + end + RUBY + + # Create a version which contains an error during loading + child_constant_with_error = child_constant.sub("# no_such_method_as_this", "no_such_method_as_this") + + fixtures_path = File.join(__dir__, "autoloading_fixtures") + Dir.mktmpdir(nil, fixtures_path) do |tmpdir| + # Set up the file structure where constants will be loaded from + child_constant_path = "#{tmpdir}/another_constant/reload_error.rb" + File.write("#{tmpdir}/constant_reload_error.rb", parent_constant) + Dir.mkdir("#{tmpdir}/another_constant") + File.write(child_constant_path, child_constant_with_error) + + tmpdir_name = tmpdir.split("/").last + with_loading("autoloading_fixtures/#{tmpdir_name}") do + # Load the file, with the error: + assert_raises(NameError) { + ConstantReloadError + } + + Timeout.timeout(0.1) do + # Remove the constant, as if Rails development middleware is reloading changed files: + ActiveSupport::Dependencies.remove_unloadable_constants! + refute defined?(AnotherConstant::ReloadError) + end + + # Change the file, so that it is **correct** this time: + File.write(child_constant_path, child_constant) + + # Again: Remove the constant, as if Rails development middleware is reloading changed files: + ActiveSupport::Dependencies.remove_unloadable_constants! + refute defined?(AnotherConstant::ReloadError) + + # Now, reload the _fixed_ constant: + assert ConstantReloadError + assert AnotherConstant::ReloadError + end + end + end + def test_module_loading with_autoloading_fixtures do assert_kind_of Module, A @@ -752,7 +807,7 @@ class DependenciesTest < ActiveSupport::TestCase Object.const_set :C, Class.new { def self.before_remove_const; end } C.unloadable assert_called(C, :before_remove_const, times: 1) do - assert C.respond_to?(:before_remove_const) + assert_respond_to C, :before_remove_const ActiveSupport::Dependencies.clear assert !defined?(C) end diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb index 04e2325754..439e117c1d 100644 --- a/activesupport/test/deprecation/method_wrappers_test.rb +++ b/activesupport/test/deprecation/method_wrappers_test.rb @@ -8,6 +8,16 @@ class MethodWrappersTest < ActiveSupport::TestCase @klass = Class.new do def new_method; "abc" end alias_method :old_method, :new_method + + protected + + def new_protected_method; "abc" end + alias_method :old_protected_method, :new_protected_method + + private + + def new_private_method; "abc" end + alias_method :old_private_method, :new_private_method end end @@ -33,4 +43,16 @@ class MethodWrappersTest < ActiveSupport::TestCase assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method } end + + def test_deprecate_methods_protected_method + ActiveSupport::Deprecation.deprecate_methods(@klass, old_protected_method: :new_protected_method) + + assert(@klass.protected_method_defined?(:old_protected_method)) + end + + def test_deprecate_methods_private_method + ActiveSupport::Deprecation.deprecate_methods(@klass, old_private_method: :new_private_method) + + assert(@klass.private_method_defined?(:old_private_method)) + end end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index f2267a822f..60673c032b 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -158,7 +158,7 @@ class DeprecationTest < ActiveSupport::TestCase stderr_output = capture(:stderr) { assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } - assert stderr_output.empty? + assert_empty stderr_output end def test_default_notify_behavior diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb index 1f8b4a8605..2c94c3c56c 100644 --- a/activesupport/test/descendants_tracker_test_cases.rb +++ b/activesupport/test/descendants_tracker_test_cases.rb @@ -37,7 +37,7 @@ module DescendantsTrackerTestCases mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear ALL.each do |k| - assert ActiveSupport::DescendantsTracker.descendants(k).empty? + assert_empty ActiveSupport::DescendantsTracker.descendants(k) end end end diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb index 7c396b7c8e..d4fedb5a67 100644 --- a/activesupport/test/descendants_tracker_with_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_with_autoloading_test.rb @@ -12,7 +12,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear ALL.each do |k| - assert ActiveSupport::DescendantsTracker.descendants(k).empty? + assert_empty ActiveSupport::DescendantsTracker.descendants(k) end end end diff --git a/activesupport/test/digest_test.rb b/activesupport/test/digest_test.rb new file mode 100644 index 0000000000..83ff2a8d83 --- /dev/null +++ b/activesupport/test/digest_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "openssl" + +class DigestTest < ActiveSupport::TestCase + class InvalidDigest; end + def test_with_default_hash_digest_class + assert_equal ::Digest::MD5.hexdigest("hello friend"), ActiveSupport::Digest.hexdigest("hello friend") + end + + def test_with_custom_hash_digest_class + original_hash_digest_class = ActiveSupport::Digest.hash_digest_class + + ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1 + digest = ActiveSupport::Digest.hexdigest("hello friend") + + assert_equal 32, digest.length + assert_equal ::Digest::SHA1.hexdigest("hello friend")[0...32], digest + ensure + ActiveSupport::Digest.hash_digest_class = original_hash_digest_class + end + + def test_should_raise_argument_error_if_custom_digest_is_missing_hexdigest_method + assert_raises(ArgumentError) { ActiveSupport::Digest.hash_digest_class = InvalidDigest } + end +end diff --git a/activesupport/test/encrypted_configuration_test.rb b/activesupport/test/encrypted_configuration_test.rb index 471faa8c12..93ccf457de 100644 --- a/activesupport/test/encrypted_configuration_test.rb +++ b/activesupport/test/encrypted_configuration_test.rb @@ -10,8 +10,10 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase @credentials_key_path = File.join(Dir.tmpdir, "master.key") File.write(@credentials_key_path, ActiveSupport::EncryptedConfiguration.generate_key) - @credentials = ActiveSupport::EncryptedConfiguration.new \ - config_path: @credentials_config_path, key_path: @credentials_key_path, env_key: "RAILS_MASTER_KEY" + @credentials = ActiveSupport::EncryptedConfiguration.new( + config_path: @credentials_config_path, key_path: @credentials_key_path, + env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true + ) end teardown do @@ -51,6 +53,14 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase assert_equal "things", @credentials[:new] end + test "raise error when writing an invalid format value" do + assert_raise(Psych::SyntaxError) do + @credentials.change do |config_file| + config_file.write "login: *login\n username: dummy" + end + end + end + test "raises key error when accessing config via bang method" do assert_raise(KeyError) { @credentials.something! } end diff --git a/activesupport/test/encrypted_file_test.rb b/activesupport/test/encrypted_file_test.rb index 7259726d08..ba3bbef903 100644 --- a/activesupport/test/encrypted_file_test.rb +++ b/activesupport/test/encrypted_file_test.rb @@ -12,8 +12,9 @@ class EncryptedFileTest < ActiveSupport::TestCase @key_path = File.join(Dir.tmpdir, "content.txt.key") File.write(@key_path, ActiveSupport::EncryptedFile.generate_key) - @encrypted_file = ActiveSupport::EncryptedFile.new \ - content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY" + @encrypted_file = ActiveSupport::EncryptedFile.new( + content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY", raise_if_missing_key: true + ) end teardown do @@ -47,4 +48,12 @@ class EncryptedFileTest < ActiveSupport::TestCase assert_equal "#{@content} and went by the lake", @encrypted_file.read end + + test "raise MissingKeyError when key is missing" do + assert_raise(ActiveSupport::EncryptedFile::MissingKeyError) do + ActiveSupport::EncryptedFile.new( + content_path: @content_path, key_path: "", env_key: "", raise_if_missing_key: true + ).read + end + end end diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index 9b560f7f42..d3af0dbef3 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -39,18 +39,18 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) {} - assert !checker.updated? + assert_not_predicate checker, :updated? # Pipes used for flow control across fork. boot_reader, boot_writer = IO.pipe touch_reader, touch_writer = IO.pipe pid = fork do - assert checker.updated? + assert_predicate checker, :updated? # Clear previous check value. checker.execute - assert !checker.updated? + assert_not_predicate checker, :updated? # Fork is booted, ready for file to be touched # notify parent process. @@ -60,7 +60,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase # has been touched. IO.select([touch_reader]) - assert checker.updated? + assert_predicate checker, :updated? end assert pid @@ -72,7 +72,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase # Notify fork that files have been touched. touch_writer.write("touched") - assert checker.updated? + assert_predicate checker, :updated? Process.wait(pid) end diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb index f8266dac06..daf7f9d139 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -89,12 +89,12 @@ module FileUpdateCheckerSharedTests i = 0 checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? touch(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? end test "updated should become true when watched files are modified" do @@ -103,12 +103,12 @@ module FileUpdateCheckerSharedTests FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? touch(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? end test "updated should become true when watched files are deleted" do @@ -117,12 +117,12 @@ module FileUpdateCheckerSharedTests FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? rm_f(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? end test "should be robust to handle files with wrong modified time" do @@ -164,14 +164,14 @@ module FileUpdateCheckerSharedTests i = 0 checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? touch(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? checker.execute - assert !checker.updated? + assert_not_predicate checker, :updated? end test "should execute the block if files change in a watched directory one extension" do diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 6cc1039d8e..5e50acf5db 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -224,6 +224,12 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("json_html_api", ActiveSupport::Inflector.underscore("JSONHTMLAPI")) end + def test_acronym_regexp_is_deprecated + assert_deprecated do + ActiveSupport::Inflector.inflections.acronym_regex + end + end + def test_underscore CamelToUnderscore.each do |camel, underscore| assert_equal(underscore, ActiveSupport::Inflector.underscore(camel)) @@ -454,12 +460,12 @@ class InflectorTest < ActiveSupport::TestCase ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear } - assert ActiveSupport::Inflector.inflections(:es).plurals.empty? - assert ActiveSupport::Inflector.inflections(:es).singulars.empty? - assert ActiveSupport::Inflector.inflections(:es).uncountables.empty? - assert !ActiveSupport::Inflector.inflections.plurals.empty? - assert !ActiveSupport::Inflector.inflections.singulars.empty? - assert !ActiveSupport::Inflector.inflections.uncountables.empty? + assert_empty ActiveSupport::Inflector.inflections(:es).plurals + assert_empty ActiveSupport::Inflector.inflections(:es).singulars + assert_empty ActiveSupport::Inflector.inflections(:es).uncountables + assert_not_empty ActiveSupport::Inflector.inflections.plurals + assert_not_empty ActiveSupport::Inflector.inflections.singulars + assert_not_empty ActiveSupport::Inflector.inflections.uncountables end def test_clear_all @@ -472,10 +478,10 @@ class InflectorTest < ActiveSupport::TestCase inflect.clear :all - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? + assert_empty inflect.plurals + assert_empty inflect.singulars + assert_empty inflect.uncountables + assert_empty inflect.humans end end @@ -489,10 +495,10 @@ class InflectorTest < ActiveSupport::TestCase inflect.clear - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? + assert_empty inflect.plurals + assert_empty inflect.singulars + assert_empty inflect.uncountables + assert_empty inflect.humans end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 96ad8dfbdb..340a2abf75 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -187,7 +187,7 @@ class TestJSONEncoding < ActiveSupport::TestCase def test_array_should_pass_encoding_options_to_children_in_as_json people = [ { name: "John", address: { city: "London", country: "UK" } }, - { name: "Jean", address: { city: "Paris" , country: "France" } } + { name: "Jean", address: { city: "Paris", country: "France" } } ] json = people.as_json only: [:address, :city] expected = [ @@ -201,7 +201,7 @@ class TestJSONEncoding < ActiveSupport::TestCase def test_array_should_pass_encoding_options_to_children_in_to_json people = [ { name: "John", address: { city: "London", country: "UK" } }, - { name: "Jean", address: { city: "Paris" , country: "France" } } + { name: "Jean", address: { city: "Paris", country: "France" } } ] json = people.to_json only: [:address, :city] @@ -210,10 +210,10 @@ class TestJSONEncoding < ActiveSupport::TestCase People = Class.new(BasicObject) do include Enumerable - def initialize() + def initialize @people = [ { name: "John", address: { city: "London", country: "UK" } }, - { name: "Jean", address: { city: "Paris" , country: "France" } } + { name: "Jean", address: { city: "Paris", country: "France" } } ] end def each(*, &blk) diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index f51fbe2671..560b86b1a3 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -469,10 +469,10 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_respond_to_knows_which_methods_the_proxy_responds_to - assert "".mb_chars.respond_to?(:slice) # Defined on Chars - assert "".mb_chars.respond_to?(:capitalize!) # Defined on Chars - assert "".mb_chars.respond_to?(:gsub) # Defined on String - assert !"".mb_chars.respond_to?(:undefined_method) # Not defined + assert_respond_to "".mb_chars, :slice # Defined on Chars + assert_respond_to "".mb_chars, :capitalize! # Defined on Chars + assert_respond_to "".mb_chars, :gsub # Defined on String + assert_not_respond_to "".mb_chars, :undefined_method # Not defined end def test_method_works_for_proxyed_methods @@ -485,7 +485,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_acts_like_string - assert "Bambi".mb_chars.acts_like_string? + assert_predicate "Bambi".mb_chars, :acts_like_string? end end diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb index 1d76c91d30..d5c9e82e9f 100644 --- a/activesupport/test/notifications/instrumenter_test.rb +++ b/activesupport/test/notifications/instrumenter_test.rb @@ -47,13 +47,13 @@ module ActiveSupport def test_start instrumenter.start("foo", payload) assert_equal [["foo", instrumenter.id, payload]], notifier.starts - assert_predicate notifier.finishes, :empty? + assert_empty notifier.finishes end def test_finish instrumenter.finish("foo", payload) assert_equal [["foo", instrumenter.id, payload]], notifier.finishes - assert_predicate notifier.starts, :empty? + assert_empty notifier.starts end end end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 5cfbd60131..28a02aafc8 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -250,8 +250,8 @@ module Notifications time = Time.now event = event(:foo, time, time + 0.01, random_id, {}) - assert_equal :foo, event.name - assert_equal time, event.time + assert_equal :foo, event.name + assert_equal time, event.time assert_in_delta 10.0, event.duration, 0.00001 end diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb index 6d6958be49..365fa96f4d 100644 --- a/activesupport/test/number_helper_i18n_test.rb +++ b/activesupport/test/number_helper_i18n_test.rb @@ -35,7 +35,7 @@ module ActiveSupport thousand: "t", million: "m", billion: "b", - trillion: "t" , + trillion: "t", quadrillion: "q" } } @@ -77,10 +77,10 @@ module ActiveSupport end def test_number_with_i18n_precision - #Delimiter was set to "" + # Delimiter was set to "" assert_equal("10000", number_to_rounded(10000, locale: "ts")) - #Precision inherited and significant was set + # Precision inherited and significant was set assert_equal("1.00", number_to_rounded(1.0, locale: "ts")) end @@ -90,7 +90,7 @@ module ActiveSupport end def test_number_with_i18n_delimiter - #Delimiter "," and separator "." + # Delimiter "," and separator "." assert_equal("1,000,000.234", number_to_delimited(1000000.234, locale: "ts")) end @@ -114,7 +114,7 @@ module ActiveSupport end def test_number_to_i18n_human_size - #b for bytes and k for kbytes + # b for bytes and k for kbytes assert_equal("2 k", number_to_human_size(2048, locale: "ts")) assert_equal("42 b", number_to_human_size(42, locale: "ts")) end @@ -125,11 +125,11 @@ module ActiveSupport end def test_number_to_human_with_default_translation_scope - #Using t for thousand + # Using t for thousand assert_equal "2 t", number_to_human(2000, locale: "ts") - #Significant was set to true with precision 2, using b for billion + # Significant was set to true with precision 2, using b for billion assert_equal "1.2 b", number_to_human(1234567890, locale: "ts") - #Using pluralization (Ten/Tens and Tenth/Tenths) + # Using pluralization (Ten/Tens and Tenth/Tenths) assert_equal "1 Tenth", number_to_human(0.1, locale: "ts") assert_equal "1.3 Tenth", number_to_human(0.134, locale: "ts") assert_equal "2 Tenths", number_to_human(0.2, locale: "ts") @@ -144,7 +144,7 @@ module ActiveSupport end def test_number_to_human_with_custom_translation_scope - #Significant was set to true with precision 2, with custom translated units + # Significant was set to true with precision 2, with custom translated units assert_equal "4.3 cm", number_to_human(0.0432, locale: "ts", units: :custom_units_for_number_to_human) end end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index b4795b6ee1..16ccc5572c 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -260,7 +260,7 @@ module ActiveSupport assert_equal "40 KB", number_helper.number_to_human_size(41100, precision: 2) assert_equal "1.0 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 2, strip_insignificant_zeros: false) assert_equal "1.012 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 3, significant: false) - assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 0, significant: true) #ignores significant it precision is 0 + assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 0, significant: true) # ignores significant it precision is 0 end end @@ -292,7 +292,7 @@ module ActiveSupport assert_equal "489.0 Thousand", number_helper.number_to_human(489000, precision: 4, strip_insignificant_zeros: false) assert_equal "1.2346 Million", number_helper.number_to_human(1234567, precision: 4, significant: false) assert_equal "1,2 Million", number_helper.number_to_human(1234567, precision: 1, significant: false, separator: ",") - assert_equal "1 Million", number_helper.number_to_human(1234567, precision: 0, significant: true, separator: ",") #significant forced to false + assert_equal "1 Million", number_helper.number_to_human(1234567, precision: 0, significant: true, separator: ",") # significant forced to false assert_equal "1 Million", number_helper.number_to_human(999999) assert_equal "1 Billion", number_helper.number_to_human(999999999) end @@ -300,13 +300,13 @@ module ActiveSupport def test_number_to_human_with_custom_units [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| - #Only integers + # Only integers volume = { unit: "ml", thousand: "lt", million: "m3" } assert_equal "123 lt", number_helper.number_to_human(123456, units: volume) assert_equal "12 ml", number_helper.number_to_human(12, units: volume) assert_equal "1.23 m3", number_helper.number_to_human(1234567, units: volume) - #Including fractionals + # Including fractionals distance = { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" } assert_equal "1.23 mm", number_helper.number_to_human(0.00123, units: distance) assert_equal "1.23 cm", number_helper.number_to_human(0.0123, units: distance) @@ -319,7 +319,7 @@ module ActiveSupport assert_equal "1.23 km", number_helper.number_to_human(1230, units: distance) assert_equal "12.3 km", number_helper.number_to_human(12300, units: distance) - #The quantifiers don't need to be a continuous sequence + # The quantifiers don't need to be a continuous sequence gangster = { hundred: "hundred bucks", million: "thousand quids" } assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster) assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster) @@ -329,11 +329,11 @@ module ActiveSupport assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster) assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster) - #Spaces are stripped from the resulting string + # Spaces are stripped from the resulting string assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " }) assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " }) - #Uses only the provided units and does not try to use larger ones + # Uses only the provided units and does not try to use larger ones assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" }) end end diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index 2c67bb02ac..cba5b8a8de 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -82,8 +82,8 @@ class OrderedOptionsTest < ActiveSupport::TestCase def test_introspection a = ActiveSupport::OrderedOptions.new - assert a.respond_to?(:blah) - assert a.respond_to?(:blah=) + assert_respond_to a, :blah + assert_respond_to a, :blah= assert_equal 42, a.method(:blah=).call(42) assert_equal 42, a.method(:blah).call end @@ -91,7 +91,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase def test_raises_with_bang a = ActiveSupport::OrderedOptions.new a[:foo] = :bar - assert a.respond_to?(:foo!) + assert_respond_to a, :foo! assert_nothing_raised { a.foo! } assert_equal a.foo, a.foo! diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 05c2fb59be..1c19c92bb0 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -39,7 +39,7 @@ class SafeBufferTest < ActiveSupport::TestCase end test "Should be considered safe" do - assert @buffer.html_safe? + assert_predicate @buffer, :html_safe? end test "Should return a safe buffer when calling to_s" do @@ -78,13 +78,13 @@ class SafeBufferTest < ActiveSupport::TestCase test "Should not return safe buffer from gsub" do altered_buffer = @buffer.gsub("", "asdf") assert_equal "asdf", altered_buffer - assert !altered_buffer.html_safe? + 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 !@buffer.html_safe? + assert_not_predicate @buffer, :html_safe? end test "Should escape dirty buffers on add" do @@ -101,13 +101,13 @@ class SafeBufferTest < ActiveSupport::TestCase test "Should preserve html_safe? status on copy" do @buffer.gsub!("", "<>") - assert !@buffer.dup.html_safe? + assert_not_predicate @buffer.dup, :html_safe? end test "Should return safe buffer when added with another safe buffer" do clean = "<script>".html_safe result_buffer = @buffer + clean - assert result_buffer.html_safe? + assert_predicate result_buffer, :html_safe? assert_equal "<script>", result_buffer end @@ -127,8 +127,8 @@ class SafeBufferTest < ActiveSupport::TestCase end test "clone_empty keeps the original dirtyness" do - assert @buffer.clone_empty.html_safe? - assert !@buffer.gsub!("", "").clone_empty.html_safe? + assert_predicate @buffer.clone_empty, :html_safe? + assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe? end test "Should be safe when sliced if original value was safe" do diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb index efd2bcfa0f..0a607594a2 100644 --- a/activesupport/test/security_utils_test.rb +++ b/activesupport/test/security_utils_test.rb @@ -9,8 +9,14 @@ class SecurityUtilsTest < ActiveSupport::TestCase assert_not ActiveSupport::SecurityUtils.secure_compare("a", "b") end - def test_variable_size_secure_compare_should_perform_string_comparison - assert ActiveSupport::SecurityUtils.variable_size_secure_compare("a", "a") - assert_not ActiveSupport::SecurityUtils.variable_size_secure_compare("a", "b") + def test_fixed_length_secure_compare_should_perform_string_comparison + assert ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "a") + assert !ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") + end + + def test_fixed_length_secure_compare_raise_on_length_mismatch + assert_raises(ArgumentError, "string length mismatch.") do + ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "ab") + end end end diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb index bf8f878a32..09726b144a 100644 --- a/activesupport/test/string_inquirer_test.rb +++ b/activesupport/test/string_inquirer_test.rb @@ -8,11 +8,11 @@ class StringInquirerTest < ActiveSupport::TestCase end def test_match - assert @string_inquirer.production? + assert_predicate @string_inquirer, :production? end def test_miss - assert_not @string_inquirer.development? + assert_not_predicate @string_inquirer, :development? end def test_missing_question_mark diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index 5fd6a6b316..e2b41cf8ee 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -21,7 +21,7 @@ class TaggedLoggingTest < ActiveSupport::TestCase assert_nil logger.formatter ActiveSupport::TaggedLogging.new(logger) assert_not_nil logger.formatter - assert logger.formatter.respond_to?(:tagged) + assert_respond_to logger.formatter, :tagged end test "tagged once" do @@ -74,11 +74,12 @@ class TaggedLoggingTest < ActiveSupport::TestCase test "keeps each tag in their own thread" do @logger.tagged("BCX") do Thread.new do + @logger.info "Dull story" @logger.tagged("OMG") { @logger.info "Cool story" } end.join @logger.info "Funky time" end - assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string + assert_equal "Dull story\n[OMG] Cool story\n[BCX] Funky time\n", @output.string end test "keeps each tag in their own instance" do diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 84e4953fe2..eced622137 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -115,6 +115,35 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end + def test_hash_of_expressions + assert_difference "@object.num" => 1, "@object.num + 1" => 1 do + @object.increment + end + end + + def test_hash_of_expressions_with_message + error = assert_raises Minitest::Assertion do + assert_difference({ "@object.num" => 0 }, "Object Changed") do + @object.increment + end + end + assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message + end + + def test_hash_of_lambda_expressions + assert_difference -> { @object.num } => 1, -> { @object.num + 1 } => 1 do + @object.increment + end + end + + def test_hash_of_expressions_identify_failure + assert_raises(Minitest::Assertion) do + assert_difference "@object.num" => 1, "1 + 1" => 1 do + @object.increment + end + end + end + def test_assert_changes_pass assert_changes "@object.num" do @object.increment @@ -156,6 +185,16 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end + def test_assert_changes_with_to_option_but_no_change_has_special_message + error = assert_raises Minitest::Assertion do + assert_changes "@object.num", to: 0 do + # no changes + end + end + + assert_equal "\"@object.num\" didn't change. It was already 0", error.message + end + def test_assert_changes_with_wrong_to_option assert_raises Minitest::Assertion do assert_changes "@object.num", to: 2 do @@ -218,6 +257,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase def test_assert_changes_with_message error = assert_raises Minitest::Assertion do assert_changes "@object.num", "@object.num should 1", to: 1 do + @object.decrement end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 4172c18ead..9c2c635f43 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -98,7 +98,7 @@ class TimeTravelTest < ActiveSupport::TestCase travel_to outer_expected_time do e = assert_raises(RuntimeError) do travel_to(inner_expected_time) do - #noop + # noop end end assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing\./, e.message) diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index acb0ecd226..405c8f315b 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -32,6 +32,12 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_period_for_local_with_ambigiuous_time + zone = ActiveSupport::TimeZone["Moscow"] + period = zone.period_for_local(Time.utc(2015, 1, 1)) + assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0)) + end + def test_from_integer_to_map assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[-28800] # PST end @@ -195,6 +201,11 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal "EDT", twz.zone end + def test_local_with_ambiguous_time + zone = ActiveSupport::TimeZone["Moscow"] + assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.local(2014, 10, 26, 1, 0, 0) + end + def test_at zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] secs = 946684800.0 @@ -303,6 +314,11 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_iso8601_with_ambiguous_time + zone = ActiveSupport::TimeZone["Moscow"] + assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.parse("2014-10-26T01:00:00") + end + def test_parse zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.parse("1999-12-31 19:00:00") @@ -412,6 +428,11 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal "argument out of range", exception.message end + def test_parse_with_ambiguous_time + zone = ActiveSupport::TimeZone["Moscow"] + assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.parse("2014-10-26 01:00:00") + end + def test_rfc3339 zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.rfc3339("1999-12-31T14:00:00-10:00") @@ -604,6 +625,11 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_strptime_with_ambiguous_time + zone = ActiveSupport::TimeZone["Moscow"] + assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.strptime("2014-10-26 01:00:00", "%Y-%m-%d %H:%M:%S") + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get("America/New_York") zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) @@ -692,6 +718,13 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_all_uninfluenced_by_time_zone_lookups_delegated_to_tzinfo + ActiveSupport::TimeZone.clear + galapagos = ActiveSupport::TimeZone["Pacific/Galapagos"] + all_zones = ActiveSupport::TimeZone.all + assert_not_includes all_zones, galapagos + end + def test_index assert_nil ActiveSupport::TimeZone["bogus"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"] diff --git a/activesupport/test/xml_mini/libxml_engine_test.rb b/activesupport/test/xml_mini/libxml_engine_test.rb index 5c13f493f1..3eef3946a3 100644 --- a/activesupport/test/xml_mini/libxml_engine_test.rb +++ b/activesupport/test/xml_mini/libxml_engine_test.rb @@ -6,7 +6,7 @@ XMLMiniEngineTest.run_with_gem("libxml") do class LibxmlEngineTest < XMLMiniEngineTest def setup super - LibXML::XML::Error.set_handler(&lambda { |error| }) #silence libxml, exceptions will do + LibXML::XML::Error.set_handler(&lambda { |error| }) # silence libxml, exceptions will do end private diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index 5e46406a7b..18a3f2ca66 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -114,7 +114,7 @@ module XmlMiniTest end test "#to_tag accepts decimal types" do - @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options) + @xml.to_tag(:b, BigDecimal("1.2"), @options) assert_xml("<b type=\"decimal\">1.2</b>") end diff --git a/ci/qunit-selenium-runner.rb b/ci/qunit-selenium-runner.rb index 3a58377d77..05bcab8cdb 100644 --- a/ci/qunit-selenium-runner.rb +++ b/ci/qunit-selenium-runner.rb @@ -6,6 +6,7 @@ require "chromedriver/helper" driver_options = Selenium::WebDriver::Chrome::Options.new driver_options.add_argument("--headless") driver_options.add_argument("--disable-gpu") +driver_options.add_argument("--no-sandbox") driver = ::Selenium::WebDriver.for(:chrome, options: driver_options) result = QUnit::Selenium::TestRunner.new(driver).open(ARGV[0], timeout: 60) diff --git a/ci/travis.rb b/ci/travis.rb index b124358789..861063afa5 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -135,7 +135,7 @@ class Build if activesupport? && !isolated? # There is a known issue with the listen tests that causes files to be # incorrectly GC'ed even when they are still in-use. The current solution - # is to only run them in isolation to avoid randomly failing our test suite. + # is to only run them in isolation to avoid random failures of our test suite. { "LISTEN" => "0" } else {} @@ -159,7 +159,7 @@ results = {} ENV["GEM"].split(",").each do |gem| [false, true].each do |isolated| next if ENV["TRAVIS_PULL_REQUEST"] && ENV["TRAVIS_PULL_REQUEST"] != "false" && isolated - next if RUBY_VERSION < "2.4" && isolated + next if RUBY_VERSION < "2.5" && isolated next if gem == "railties" && isolated next if gem == "ac" && isolated next if gem == "ac:integration" && isolated diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index d8b122d264..64e8b51817 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1 +1,3 @@ -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/guides/CHANGELOG.md) for previous changes. + + +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md) for previous changes. diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/belongs_to.png Binary files differindex 1a9926e578..2b8c1d52ea 100644 --- a/guides/assets/images/belongs_to.png +++ b/guides/assets/images/belongs_to.png diff --git a/guides/assets/images/habtm.png b/guides/assets/images/habtm.png Binary files differindex 41013b743d..7e508cc1a6 100644 --- a/guides/assets/images/habtm.png +++ b/guides/assets/images/habtm.png diff --git a/guides/assets/images/has_many.png b/guides/assets/images/has_many.png Binary files differindex 0d67bea38b..36ccf9f0f6 100644 --- a/guides/assets/images/has_many.png +++ b/guides/assets/images/has_many.png diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/has_many_through.png Binary files differindex b4da60e1fb..9e9caabd73 100644 --- a/guides/assets/images/has_many_through.png +++ b/guides/assets/images/has_many_through.png diff --git a/guides/assets/images/has_one.png b/guides/assets/images/has_one.png Binary files differindex c70763856a..c29c6b9c59 100644 --- a/guides/assets/images/has_one.png +++ b/guides/assets/images/has_one.png diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/has_one_through.png Binary files differindex 888a02b775..fdf13286c4 100644 --- a/guides/assets/images/has_one_through.png +++ b/guides/assets/images/has_one_through.png diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/polymorphic.png Binary files differindex e0a7f6d64a..d630db9e01 100644 --- a/guides/assets/images/polymorphic.png +++ b/guides/assets/images/polymorphic.png diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 932d329943..ffd81c0079 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -gem "bundler", "< 1.16" - begin require "bundler/inline" rescue LoadError => e @@ -15,7 +13,6 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" end require "action_controller/railtie" diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb index 36d9137b71..4bcee07607 100644 --- a/guides/bug_report_templates/active_job_master.rb +++ b/guides/bug_report_templates/active_job_master.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -gem "bundler", "< 1.16" - begin require "bundler/inline" rescue LoadError => e @@ -15,7 +13,6 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" end require "active_job" diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index b66deb36f3..914f04f51a 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -gem "bundler", "< 1.16" - begin require "bundler/inline" rescue LoadError => e @@ -15,7 +13,6 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb index 737ce66d7b..194b093ac3 100644 --- a/guides/bug_report_templates/active_record_migrations_master.rb +++ b/guides/bug_report_templates/active_record_migrations_master.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -gem "bundler", "< 1.16" - begin require "bundler/inline" rescue LoadError => e @@ -15,7 +13,6 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" gem "sqlite3" end diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb index f5c88086a9..046572148b 100644 --- a/guides/bug_report_templates/benchmark.rb +++ b/guides/bug_report_templates/benchmark.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -gem "bundler", "< 1.16" - begin require "bundler/inline" rescue LoadError => e @@ -15,7 +13,6 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" gem "benchmark-ips" end diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index 240571ba9a..727f428960 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -gem "bundler", "< 1.16" - begin require "bundler/inline" rescue LoadError => e @@ -15,7 +13,6 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" end require "active_support" diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 1f2fe91ea1..78820a7856 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -75,7 +75,7 @@ HTML # # It is important that we do not eat more than one newline # because formatting may be wrong otherwise. For example, - # if a bulleted list follows the first item is not rendered + # if a bulleted list follows, the first item is not rendered # as a list item, but as a paragraph starting with a plain # asterisk. body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index ac5833e069..afe0550a17 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -125,7 +125,7 @@ There are two big additions to talk about here: transactional migrations and poo Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by `rake db:migrate:redo` after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone. In Rails 2.2, transactional migrations are supported on PostgreSQL out of the box. The code is extensible to other database types in the future - and IBM has already extended it to support the DB2 adapter. -* Lead Contributor: [Adam Wiggins](http://adam.heroku.com/) +* Lead Contributor: [Adam Wiggins](http://about.adamwiggins.com/) * More information: * [DDL Transactions](http://adam.heroku.com/past/2008/9/3/ddl_transactions/) * [A major milestone for DB2 on Rails](http://db2onrails.com/2008/11/08/a-major-milestone-for-db2-on-rails/) @@ -391,7 +391,7 @@ You can unpack or install a single gem by specifying `GEM=_gem_name_` on the com * Lead Contributor: [Matt Jones](https://github.com/al2o3cr) * More information: * [What's New in Edge Rails: Gem Dependencies](http://archives.ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies) - * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](http://afreshcup.com/2008/10/25/rails-212-and-22rc1-update-your-rubygems/) + * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](https://afreshcup.com/home/2008/10/25/rails-212-and-22rc1-update-your-rubygems) * [Detailed discussion on Lighthouse](http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1128) ### Other Railties Changes diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index 1020f4a8e7..634569fa2d 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -234,7 +234,7 @@ Rails chooses between file, template, and action depending on whether there is a If you're one of the people who has always been bothered by the special-case naming of `application.rb`, rejoice! It's been reworked to be `application_controller.rb` in Rails 2.3. In addition, there's a new rake task, `rake rails:update:application_controller` to do this automatically for you - and it will be run as part of the normal `rake rails:update` process. * More Information: - * [The Death of Application.rb](http://afreshcup.com/2008/11/17/rails-2x-the-death-of-applicationrb/) + * [The Death of Application.rb](https://afreshcup.com/home/2008/11/17/rails-2x-the-death-of-applicationrb) * [What's New in Edge Rails: Application.rb Duality is no More](http://archives.ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more) ### HTTP Digest Authentication Support @@ -468,7 +468,7 @@ options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lamb ``` * Lead Contributor: [Tekin Suleyman](http://tekin.co.uk/) -* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](http://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections/) +* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](https://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections) ### A Note About Template Loading @@ -533,7 +533,7 @@ If you look up the spec on the "json.org" site, you'll discover that all keys in ### Other Active Support Changes * You can use `Enumerable#none?` to check that none of the elements match the supplied block. -* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil. +* If you're using Active Support [delegates](https://afreshcup.com/home/2008/10/19/coming-in-rails-22-delegate-prefixes) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil. * `ActiveSupport::OrderedHash`: now implements `each_key` and `each_value`. * `ActiveSupport::MessageEncryptor` provides a simple way to encrypt information for storage in an untrusted location (like cookies). * Active Support's `from_xml` no longer depends on XmlSimple. Instead, Rails now includes its own XmlMini implementation, with just the functionality that it requires. This lets Rails dispense with the bundled copy of XmlSimple that it's been carting around. @@ -592,7 +592,7 @@ The internals of the various <code>rake gem</code> tasks have been substantially * Internal Rails testing has been switched from `Test::Unit::TestCase` to `ActiveSupport::TestCase`, and the Rails core requires Mocha to test. * The default `environment.rb` file has been decluttered. * The dbconsole script now lets you use an all-numeric password without crashing. -* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](http://afreshcup.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`. +* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](https://afreshcup.wordpress.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`. * Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`). * Rails Guides have been converted from AsciiDoc to Textile markup. * Scaffolded views and controllers have been cleaned up a bit. @@ -605,7 +605,7 @@ Deprecated A few pieces of older code are deprecated in this release: -* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts/tree) plugin. +* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts) plugin. * `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](https://github.com/rails/render_component/tree/master). * Support for Rails components has been removed. * If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back. diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index f0e2cb3b63..7ffa7d4a5c 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -213,7 +213,7 @@ Railties now deprecates: More information: * [Discovering Rails 3 generators](http://blog.plataformatec.com.br/2010/01/discovering-rails-3-generators) -* [The Rails Module (in Rails 3)](http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/) +* [The Rails Module (in Rails 3)](http://quaran.to/blog/2010/02/03/the-rails-module/) Action Pack ----------- diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index f6571544f9..ae6eb27f35 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -36,7 +36,7 @@ TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. * `coffee-rails ~> 3.2.1` * `uglifier >= 1.0.3` -* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. * There are a couple of new configuration changes you'd want to add in `config/environments/development.rb`: @@ -156,7 +156,7 @@ Railties will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively. -* Turn gem has been removed from default Gemfile. +* Turn gem has been removed from default `Gemfile`. * Remove old plugin generator `rails generate plugin` in favor of `rails plugin new` command. diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 6bf65757ec..2c5e665e33 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -274,7 +274,7 @@ for detailed changes. * The [Spring application preloader](https://github.com/rails/spring) is now installed by default for new applications. It uses the development group of - the Gemfile, so will not be installed in + the `Gemfile`, so will not be installed in production. ([Pull Request](https://github.com/rails/rails/pull/12958)) * `BACKTRACE` environment variable to show unfiltered backtraces for test diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index 036a310ac8..7105df5634 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -368,7 +368,7 @@ Please refer to the [Changelog][railties] for detailed changes. ### Notable changes -* Introduced `web-console` in the default application Gemfile. +* Introduced `web-console` in the default application `Gemfile`. ([Pull Request](https://github.com/rails/rails/pull/11667)) * Added a `required` option to the model generator for associations. diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md index 6b9a950a42..852d04b1f6 100644 --- a/guides/source/5_1_release_notes.md +++ b/guides/source/5_1_release_notes.md @@ -355,6 +355,9 @@ Please refer to the [Changelog][action-pack] for detailed changes. ([Commit](https://github.com/rails/rails/commit/79a5ea9eadb4d43b62afacedc0706cbe88c54496), [Commit](https://github.com/rails/rails/commit/57e1c99a280bdc1b324936a690350320a1cd8111)) +* Removed deprecated support for calling `HashWithIndifferentAccess` methods on `ActionController::Parameters`. + ([Commit](https://github.com/rails/rails/pull/26746/commits/7093ceb480ad6a0a91b511832dad4c6a86981b93)) + ### Deprecations * Deprecated `config.action_controller.raise_on_unfiltered_parameters`. diff --git a/guides/source/5_2_release_notes.md b/guides/source/5_2_release_notes.md new file mode 100644 index 0000000000..7b5c4b87e3 --- /dev/null +++ b/guides/source/5_2_release_notes.md @@ -0,0 +1,226 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Ruby on Rails 5.2 Release Notes +=============================== + +Highlights in Rails 5.2: + +* Active Storage +* Redis Cache Store +* HTTP/2 Early hints support +* Credentials +* Default Content Security Policy + +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/5-2-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 5.2 +---------------------- + +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.1 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 5.2. + + +Major Features +-------------- + +### Active Storage + +[README](https://github.com/rails/rails/blob/d3893ec38ec61282c2598b01a298124356d6b35a/activestorage/README.md) + +### Redis Cache Store + +[Pull Request](https://github.com/rails/rails/pull/31134) + + +### HTTP/2 Early hints support + +[Pull Request](https://github.com/rails/rails/pull/30744) + + +### Credentials + +[Pull Request](https://github.com/rails/rails/pull/30067) + + +### Default Content Security Policy + +[Pull Request](https://github.com/rails/rails/pull/31162) + +Incompatibilities +----------------- + +ToDo + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Deprecations + +* Deprecate `capify!` method in generators and templates. + ([Pull Request](https://github.com/rails/rails/pull/29493)) + +* Deprecated passing the environment's name as a regular argument to the + `rails dbconsole` and `rails console` commands. + ([Pull Request](https://github.com/rails/rails/pull/29358)) + +* Deprecated using subclass of `Rails::Application` to start the Rails server. + ([Pull Request](https://github.com/rails/rails/pull/30127)) + +* Deprecated `after_bundle` callback in Rails plugin templates. + ([Pull Request](https://github.com/rails/rails/pull/29446)) + +### Notable changes + +ToDo + +Action Cable +----------- + +Please refer to the [Changelog][action-cable] for detailed changes. + +### Removals + +* Removed deprecated evented redis adapter. + ([Commit](https://github.com/rails/rails/commit/48766e32d31)) + +### Notable changes + +* Added support for `host`, `port`, `db` and `password` options in cable.yml + ([Pull Request](https://github.com/rails/rails/pull/29528)) + +* Added support for compatibility with redis-rb gem for 4.0 version. + ([Pull Request](https://github.com/rails/rails/pull/30748)) + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +* Removed deprecated `ActionController::ParamsParser::ParseError`. + ([Commit](https://github.com/rails/rails/commit/e16c765ac6d)) + +### Deprecations + +* Deprecated `#success?`, `#missing?` and `#error?` aliases of + `ActionDispatch::TestResponse`. + ([Pull Request](https://github.com/rails/rails/pull/30104)) + +### Notable changes + +ToDo + +Action View +------------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +* Removed deprecated Erubis ERB handler. + ([Commit](https://github.com/rails/rails/commit/7de7f12fd14)) + +### Deprecations + +* Deprecated `image_alt` helper which used to add default alt text to + the images generated by `image_tag`. + ([Pull Request](https://github.com/rails/rails/pull/30213)) + +### Notable changes + +ToDo + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Notable changes + +ToDo + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +ToDo + +### Deprecations + +ToDo + +### Notable changes + +ToDo + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +ToDo + +### Notable changes + +ToDo + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +ToDo + +### Deprecations + +ToDo + +### Notable changes + +ToDo + +Active Job +----------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Removals + +ToDo + +### Notable changes + +ToDo + +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/5-2-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/5-2-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 57403a4bf9..c250db2e0c 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -1,12 +1,14 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action Cable Overview ===================== -In this guide you will learn how Action Cable works and how to use WebSockets to +In this guide, you will learn how Action Cable works and how to use WebSockets to incorporate real-time features into your Rails application. After reading this guide, you will know: -* What Action Cable is and its integration on backend and frontend +* What Action Cable is and its integration backend and frontend * How to setup Action Cable * How to setup channels * Deployment and Architecture setup for running Action Cable @@ -129,7 +131,7 @@ subscriptions based on an identifier sent by the cable consumer. # app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel # Called when the consumer has successfully - # become a subscriber of this channel. + # become a subscriber to this channel. def subscribed end end @@ -225,7 +227,7 @@ A *broadcasting* is a pub/sub link where anything transmitted by a publisher is routed directly to the channel subscribers who are streaming that named broadcasting. Each channel can be streaming zero or more broadcastings. -Broadcastings are purely an online queue and time dependent. If a consumer is +Broadcastings are purely an online queue and time-dependent. If a consumer is not streaming (subscribed to a given channel), they'll not get the broadcast should they connect later. @@ -515,8 +517,8 @@ user. For a user with an ID of 1, the broadcasting name would be The channel has been instructed to stream everything that arrives at `web_notifications:1` directly to the client by invoking the `received` callback. The data passed as argument is the hash sent as the second parameter -to the server-side broadcast call, JSON encoded for the trip across the wire, -and unpacked for the data argument arriving to `received`. +to the server-side broadcast call, JSON encoded for the trip across the wire +and unpacked for the data argument arriving as `received`. ### More Complete Examples @@ -569,7 +571,7 @@ This may change in the future. [#27214](https://github.com/rails/rails/issues/27 Action Cable will only accept requests from specified origins, which are passed to the server config as an array. The origins can be instances of -strings or regular expressions, against which a check for match will be performed. +strings or regular expressions, against which a check for the match will be performed. ```ruby config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}] @@ -592,7 +594,7 @@ environment configuration files. ### Other Configurations -The other common option to configure, is the log tags applied to the +The other common option to configure is the log tags applied to the per-connection logger. Here's an example that uses the user account id if available, else "no-account" while tagging: @@ -607,7 +609,7 @@ config.action_cable.log_tags = [ For a full list of all configuration options, see the `ActionCable::Server::Configuration` class. -Also note that your server must provide at least the same number of database +Also, note that your server must provide at least the same number of database connections as you have workers. The default worker pool size is set to 4, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute. diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 28f7246197..6ecfb57db3 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -21,7 +21,7 @@ After reading this guide, you will know: What Does a Controller Do? -------------------------- -Action Controller is the C in MVC. After the router has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. +Action Controller is the C in [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). After the router has determined which controller to use for a request, the controller is responsible for making sense of the request, and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. For most conventional [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index cb07781d1c..fe31e3403f 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -805,7 +805,7 @@ config.action_mailer.smtp_settings = { user_name: '<username>', password: '<password>', authentication: 'plain', - enable_starttls_auto: true } + enable_starttls_auto: true } ``` Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. You can change your Gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled, diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index c1e02745de..1cba5c6fb6 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -149,10 +149,10 @@ end #### Jbuilder [Jbuilder](https://github.com/rails/jbuilder) is a gem that's -maintained by the Rails team and included in the default Rails Gemfile. +maintained by the Rails team and included in the default Rails `Gemfile`. It's similar to Builder, but is used to generate JSON, instead of XML. -If you don't have it, you can add the following to your Gemfile: +If you don't have it, you can add the following to your `Gemfile`: ```ruby gem 'jbuilder' @@ -807,20 +807,22 @@ The core method of this helper, `form_for`, gives you the ability to create a fo The HTML generated for this would be: ```html -<form action="/people/create" method="post"> - <input id="person_first_name" name="person[first_name]" type="text" /> - <input id="person_last_name" name="person[last_name]" type="text" /> - <input name="commit" type="submit" value="Create" /> +<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post"> + <input name="utf8" type="hidden" value="✓" /> + <input type="hidden" name="authenticity_token" value="lTuvBzs7ANygT0NFinXj98tfw3Emfm65wwYLbUvoWsK2pngccIQSUorM2C035M9dZswXgWTvKwFS8W5TVblpYw==" /> + <input type="text" name="person[first_name]" id="person_first_name" /> + <input type="text" name="person[last_name]" id="person_last_name" /> + <input type="submit" name="commit" value="Create" data-disable-with="Create" /> </form> ``` The params object created when this form is submitted would look like: ```ruby -{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } } +{"utf8" => "✓", "authenticity_token" => "lTuvBzs7ANygT0NFinXj98tfw3Emfm65wwYLbUvoWsK2pngccIQSUorM2C035M9dZswXgWTvKwFS8W5TVblpYw==", "person" => {"first_name" => "William", "last_name" => "Smith"}, "commit" => "Create", "controller" => "people", "action" => "create"} ``` -The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller. +The params hash has a nested person value, which can therefore be accessed with `params[:person]` in the controller. #### check_box diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 069a624984..2f85b765a3 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -38,13 +38,15 @@ object on how to write to and read from the database. ### Object Relational Mapping -Object Relational Mapping, commonly referred to as its abbreviation ORM, is +[Object Relational Mapping](https://en.wikipedia.org/wiki/Object-relational_mapping), commonly referred to as its abbreviation ORM, is a technique that connects the rich objects of an application to tables in a relational database management system. Using ORM, the properties and relationships of the objects in an application can be easily stored and retrieved from a database without writing SQL statements directly and with less overall database access code. +NOTE: If you are not familiar enough with relational database management systems (RDBMS) or structured query language (SQL), please go through [this tutorial](https://www.w3schools.com/sql/default.asp) (or [this one](http://www.sqlcourse.com/)) or study them by other means. Understanding how relational databases work is crucial to understanding Active Records and Rails in general. + ### Active Record as an ORM Framework Active Record gives us several mechanisms, the most important being the ability @@ -142,7 +144,7 @@ end This will create a `Product` model, mapped to a `products` table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. Suppose -that the `products` table was created using an SQL statement like: +that the `products` table was created using an SQL (or one of its extensions) statement like: ```sql CREATE TABLE products ( @@ -152,8 +154,9 @@ CREATE TABLE products ( ); ``` -Following the table schema above, you would be able to write code like the -following: +Schema above declares a table with two columns: `id` and `name`. Each row of +this table represents a certain product with these two parameters. Thus, you +would be able to write code like the following: ```ruby p = Product.new diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 53417f012e..630dafe632 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -213,6 +213,7 @@ The following methods trigger callbacks: * `save!` * `save(validate: false)` * `toggle!` +* `touch` * `update_attribute` * `update` * `update!` @@ -245,7 +246,6 @@ Just as with validations, it is also possible to skip callbacks by using the fol * `increment` * `increment_counter` * `toggle` -* `touch` * `update_column` * `update_columns` * `update_all` diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index f8f36bf600..ab3af438f5 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -353,8 +353,7 @@ create_table :products, options: "ENGINE=BLACKHOLE" do |t| end ``` -will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table -(when using MySQL or MariaDB, the default is `ENGINE=InnoDB`). +will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table. Also you can pass the `:comment` option with any description for the table that will be stored in database itself and can be viewed with database administration diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 3786343fc3..4e28e31a53 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -801,7 +801,7 @@ The SQL that would be executed: SELECT * FROM articles WHERE id > 10 ORDER BY id DESC # Original query without `only` -SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 +SELECT * FROM articles WHERE id > 10 ORDER BY id DESC LIMIT 20 ``` @@ -820,14 +820,14 @@ Article.find(10).comments.reorder('name') The SQL that would be executed: ```sql -SELECT * FROM articles WHERE id = 10 +SELECT * FROM articles WHERE id = 10 LIMIT 1 SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` In the case where the `reorder` clause is not used, the SQL executed would be: ```sql -SELECT * FROM articles WHERE id = 10 +SELECT * FROM articles WHERE id = 10 LIMIT 1 SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC ``` @@ -1091,7 +1091,7 @@ This produces: ```sql SELECT articles.* FROM articles - INNER JOIN categories ON articles.category_id = categories.id + INNER JOIN categories ON categories.id = articles.category_id INNER JOIN comments ON comments.article_id = articles.id ``` @@ -1871,14 +1871,14 @@ All calculation methods work directly on a model: ```ruby Client.count -# SELECT count(*) AS count_all FROM clients +# SELECT COUNT(*) FROM clients ``` Or on a relation: ```ruby Client.where(first_name: 'Ryan').count -# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan') +# SELECT COUNT(*) FROM clients WHERE (first_name = 'Ryan') ``` You can also use various finder methods on a relation for performing complex calculations: @@ -1890,9 +1890,9 @@ Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' Which will execute: ```sql -SELECT count(DISTINCT clients.id) AS count_all FROM clients - LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE - (clients.first_name = 'Ryan' AND orders.status = 'received') +SELECT COUNT(DISTINCT clients.id) FROM clients + LEFT OUTER JOIN orders ON orders.client_id = clients.id + WHERE (clients.first_name = 'Ryan' AND orders.status = 'received') ``` ### Count diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index e9157f3db1..d076efcd54 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -953,7 +953,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and ```ruby class Computer < ApplicationRecord validates :mouse, presence: true, - if: [Proc.new { |c| c.market.retail? }, :desktop?], + if: [Proc.new { |c| c.market.retail? }, :desktop?], unless: Proc.new { |c| c.trackpad.present? } end ``` diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md new file mode 100644 index 0000000000..97c56dfd93 --- /dev/null +++ b/guides/source/active_storage_overview.md @@ -0,0 +1,565 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Active Storage Overview +======================= + +This guide covers how to attach files to your Active Record models. + +After reading this guide, you will know: + +* How to attach one or many files to a record. +* How to delete an attached file. +* How to link to an attached file. +* How to use variants to transform images. +* How to generate an image representation of a non-image file, such as a PDF or a video. +* How to send file uploads directly from browsers to a storage service, + bypassing your application servers. +* How to clean up files stored during testing. +* How to implement support for additional storage services. + +-------------------------------------------------------------------------------- + +What is Active Storage? +----------------------- + +Active Storage facilitates uploading files to a cloud storage service like +Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those +files to Active Record objects. It comes with a local disk-based service for +development and testing and supports mirroring files to subordinate services for +backups and migrations. + +Using Active Storage, an application can transform image uploads with +[ImageMagick](https://www.imagemagick.org), generate image representations of +non-image uploads like PDFs and videos, and extract metadata from arbitrary +files. + +## Setup + +Active Storage uses two tables in your application’s database named +`active_storage_blobs` and `active_storage_attachments`. After upgrading your +application to Rails 5.2, run `rails active_storage:install` to generate a +migration that creates these tables. Use `rails db:migrate` to run the +migration. + +Declare Active Storage services in `config/storage.yml`. For each service your +application uses, provide a name and the requisite configuration. The example +below declares three services named `local`, `test`, and `amazon`: + +```yaml +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +amazon: + service: S3 + access_key_id: "" + secret_access_key: "" +``` + +Tell Active Storage which service to use by setting +`Rails.application.config.active_storage.service`. Because each environment will +likely use a different service, it is recommended to do this on a +per-environment basis. To use the disk service from the previous example in the +development environment, you would add the following to +`config/environments/development.rb`: + +```ruby +# Store files locally. +config.active_storage.service = :local +``` + +To use the Amazon S3 service in production, you add the following to +`config/environments/production.rb`: + +```ruby +# Store files on Amazon S3. +config.active_storage.service = :amazon +``` + +Continue reading for more information on the built-in service adapters (e.g. +`Disk` and `S3`) and the configuration they require. + +### Disk Service + +Declare a Disk service in `config/storage.yml`: + +```yaml +local: + service: Disk + root: <%= Rails.root.join("storage") %> +``` + +Optionally specify a host for generating URLs (the default is `http://localhost:3000`): + +```yaml +local: + service: Disk + root: <%= Rails.root.join("storage") %> + host: http://myapp.test +``` + +### Amazon S3 Service + +Declare an S3 service in `config/storage.yml`: + +```yaml +amazon: + service: S3 + access_key_id: "" + secret_access_key: "" + region: "" + bucket: "" +``` + +Add the [`aws-sdk-s3`](https://github.com/aws/aws-sdk-ruby) gem to your `Gemfile`: + +```ruby +gem "aws-sdk-s3", require: false +``` + +### Microsoft Azure Storage Service + +Declare an Azure Storage service in `config/storage.yml`: + +```yaml +azure: + service: AzureStorage + path: "" + storage_account_name: "" + storage_access_key: "" + container: "" +``` + +Add the [`azure-storage`](https://github.com/Azure/azure-storage-ruby) gem to your `Gemfile`: + +```ruby +gem "azure-storage", require: false +``` + +### Google Cloud Storage Service + +Declare a Google Cloud Storage service in `config/storage.yml`: + +```yaml +google: + service: GCS + credentials: <%= Rails.root.join("path/to/keyfile.json") %> + project: "" + bucket: "" +``` + +Optionally provide a Hash of credentials instead of a keyfile path: + +```yaml +google: + service: GCS + credentials: + type: "service_account" + project_id: "" + private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %> + private_key: <%= Rails.application.credentials.dig(:gcs, :private_key) %> + client_email: "" + client_id: "" + auth_uri: "https://accounts.google.com/o/oauth2/auth" + token_uri: "https://accounts.google.com/o/oauth2/token" + auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs" + client_x509_cert_url: "" + project: "" + bucket: "" +``` + +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 +``` + +### Mirror Service + +You can keep multiple services in sync by defining a mirror service. When a file +is uploaded or deleted, it's done across all the mirrored services. Mirrored +services can be used to facilitate a migration between services in production. +You can start mirroring to the new service, copy existing files from the old +service to the new, then go all-in on the new service. Define each of the +services you'd like to use as described above and reference them from a mirrored +service. + +```yaml +s3_west_coast: + service: S3 + access_key_id: "" + secret_access_key: "" + region: "" + bucket: "" + +s3_east_coast: + service: S3 + access_key_id: "" + secret_access_key: "" + region: "" + bucket: "" + +production: + service: Mirror + primary: s3_east_coast + mirrors: + - s3_west_coast +``` + +NOTE: Files are served from the primary service. + +Attaching Files to Records +-------------------------- + +### `has_one_attached` + +The `has_one_attached` macro sets up a one-to-one mapping between records and +files. Each record can have one file attached to it. + +For example, suppose your application has a `User` model. If you want each user to +have an avatar, define the `User` model like this: + +```ruby +class User < ApplicationRecord + has_one_attached :avatar +end +``` + +You can create a user with an avatar: + +```ruby +class SignupController < ApplicationController + def create + user = User.create!(user_params) + session[:user_id] = user.id + redirect_to root_path + end + + private + def user_params + params.require(:user).permit(:email_address, :password, :avatar) + end +end +``` + +Call `avatar.attach` to attach an avatar to an existing user: + +```ruby +Current.user.avatar.attach(params[:avatar]) +``` + +Call `avatar.attached?` to determine whether a particular user has an avatar: + +```ruby +Current.user.avatar.attached? +``` + +### `has_many_attached` + +The `has_many_attached` macro sets up a one-to-many relationship between records +and files. Each record can have many files attached to it. + +For example, suppose your application has a `Message` model. If you want each +message to have many images, define the `Message` model like this: + +```ruby +class Message < ApplicationRecord + has_many_attached :images +end +``` + +You can create a message with images: + +```ruby +class MessagesController < ApplicationController + def create + message = Message.create!(message_params) + redirect_to message + end + + private + def message_params + params.require(:message).permit(:title, :content, images: []) + end +end +``` + +Call `images.attach` to add new images to an existing message: + +```ruby +@message.images.attach(params[:images]) +``` + +Call `images.attached?` to determine whether a particular message has any images: + +```ruby +@message.images.attached? +``` + +Removing Files +-------------- + +To remove an attachment from a model, call `purge` on the attachment. Removal +can be done in the background if your application is setup to use Active Job. +Purging deletes the blob and the file from the storage service. + +```ruby +# Synchronously destroy the avatar and actual resource files. +user.avatar.purge + +# Destroy the associated models and actual resource files async, via Active Job. +user.avatar.purge_later +``` + +Linking to Files +---------------- + +Generate a permanent URL for the blob that points to the application. Upon +access, a redirect to the actual service endpoint is returned. This indirection +decouples the public URL from the actual one, and allows, for example, mirroring +attachments in different services for high-availability. The redirection has an +HTTP expiration of 5 min. + +```ruby +url_for(user.avatar) +``` + +To create a download link, use the `rails_blob_{path|url}` helper. Using this +helper allows you to set the disposition. + +```ruby +rails_blob_path(user.avatar, disposition: "attachment") +``` + +Transforming Images +------------------- + +To create variation of the image, call `variant` on the Blob. +You can pass any [MiniMagick](https://github.com/minimagick/minimagick) +supported transformation to the method. + +To enable variants, add `mini_magick` to your `Gemfile`: + +```ruby +gem 'mini_magick' +``` + +When the browser hits the variant URL, Active Storage will lazy transform the +original blob into the format you specified and redirect to its new service +location. + +```erb +<%= image_tag user.avatar.variant(resize: "100x100") %> +``` + +Previewing Files +---------------- + +Some non-image files can be previewed: that is, they can be presented as images. +For example, a video file can be previewed by extracting its first frame. Out of +the box, Active Storage supports previewing videos and PDF documents. + +```erb +<ul> + <% @message.files.each do |file| %> + <li> + <%= image_tag file.preview(resize: "100x100>") %> + </li> + <% end %> +</ul> +``` + +WARNING: Extracting previews requires third-party applications, `ffmpeg` for +video and `mutool` for PDFs. These libraries are not provided by Rails. You must +install them yourself to use the built-in previewers. Before you install and use +third-party software, make sure you understand the licensing implications of +doing so. + +Direct Uploads +-------------- + +Active Storage, with its included JavaScript library, supports uploading +directly from the client to the cloud. + +### Direct upload installation + +1. Include `activestorage.js` in your application's JavaScript bundle. + + Using the asset pipeline: + + ```js + //= require activestorage + + ``` + + Using the npm package: + + ```js + import * as ActiveStorage from "activestorage" + ActiveStorage.start() + ``` + +2. Annotate file inputs with the direct upload URL. + + ```ruby + <%= form.file_field :attachments, multiple: true, direct_upload: true %> + ``` +3. That's it! Uploads begin upon form submission. + +### Direct upload JavaScript events + +| Event name | Event target | Event data (`event.detail`) | Description | +| --- | --- | --- | --- | +| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submitted. | +| `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. | +| `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. | +| `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. | +| `direct-upload:before-storage-request` | `<input>` | `{id, file, xhr}` | Before making a request to store a file. | +| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. | +| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. | +| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. | +| `direct-uploads:end` | `<form>` | None | All direct uploads have ended. | + +### Example + +You can use these events to show the progress of an upload. + +![direct-uploads](https://user-images.githubusercontent.com/5355/28694528-16e69d0c-72f8-11e7-91a7-c0b8cfc90391.gif) + +To show the uploaded files in a form: + +```js +// direct_uploads.js + +addEventListener("direct-upload:initialize", event => { + const { target, detail } = event + const { id, file } = detail + target.insertAdjacentHTML("beforebegin", ` + <div id="direct-upload-${id}" class="direct-upload direct-upload--pending"> + <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div> + <span class="direct-upload__filename">${file.name}</span> + </div> + `) +}) + +addEventListener("direct-upload:start", event => { + const { id } = event.detail + const element = document.getElementById(`direct-upload-${id}`) + element.classList.remove("direct-upload--pending") +}) + +addEventListener("direct-upload:progress", event => { + const { id, progress } = event.detail + const progressElement = document.getElementById(`direct-upload-progress-${id}`) + progressElement.style.width = `${progress}%` +}) + +addEventListener("direct-upload:error", event => { + event.preventDefault() + const { id, error } = event.detail + const element = document.getElementById(`direct-upload-${id}`) + element.classList.add("direct-upload--error") + element.setAttribute("title", error) +}) + +addEventListener("direct-upload:end", event => { + const { id } = event.detail + const element = document.getElementById(`direct-upload-${id}`) + element.classList.add("direct-upload--complete") +}) +``` + +Add styles: + +```css +/* direct_uploads.css */ + +.direct-upload { + display: inline-block; + position: relative; + padding: 2px 4px; + margin: 0 3px 3px 0; + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 3px; + font-size: 11px; + line-height: 13px; +} + +.direct-upload--pending { + opacity: 0.6; +} + +.direct-upload__progress { + position: absolute; + top: 0; + left: 0; + bottom: 0; + opacity: 0.2; + background: #0076ff; + transition: width 120ms ease-out, opacity 60ms 60ms ease-in; + transform: translate3d(0, 0, 0); +} + +.direct-upload--complete .direct-upload__progress { + opacity: 0.4; +} + +.direct-upload--error { + border-color: red; +} + +input[type=file][data-direct-upload-url][disabled] { + display: none; +} +``` + +Discarding Files Stored During System Tests +------------------------------------------- + +System tests clean up test data by rolling back a transaction. Because destroy +is never called on an object, the attached files are never cleaned up. If you +want to clear the files, you can do it in an `after_teardown` callback. Doing it +here ensures that all connections created during the test are complete and +you won't receive an error from Active Storage saying it can't find a file. + +```ruby +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] + + def remove_uploaded_files + FileUtils.rm_rf("#{Rails.root}/storage_test") + end + + def after_teardown + super + remove_uploaded_files + end +end +``` + +If your system tests verify the deletion of a model with attachments and you're +using Active Job, set your test environment to use the inline queue adapter so +the purge job is executed immediately rather at an unknown time in the future. + +You may also want to use a separate service definition for the test environment +so your tests don't delete the files you create during development. + +```ruby +# Use inline job processing to make things happen immediately +config.active_job.queue_adapter = :inline + +# Separate file storage in the test environment +config.active_storage.service = :local_test +``` + +Implementing Support for Other Cloud Services +--------------------------------------------- + +If you need to support a cloud service other than these, you will need to +implement the Service. Each service extends +[`ActiveStorage::Service`](https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/service.rb) +by implementing the methods necessary to upload and download files to the cloud. diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 66d2fbd939..8e2826bb85 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1978,19 +1978,19 @@ Extensions to `BigDecimal` The method `to_s` provides a default specifier of "F". This means that a simple call to `to_s` will result in floating point representation instead of engineering notation: ```ruby -BigDecimal.new(5.00, 6).to_s # => "5.0" +BigDecimal(5.00, 6).to_s # => "5.0" ``` and that symbol specifiers are also supported: ```ruby -BigDecimal.new(5.00, 6).to_s(:db) # => "5.0" +BigDecimal(5.00, 6).to_s(:db) # => "5.0" ``` Engineering notation is still supported: ```ruby -BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1" +BigDecimal(5.00, 6).to_s("e") # => "0.5E1" ``` Extensions to `Enumerable` diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 25f78fd940..11c4a8222a 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -543,6 +543,13 @@ Active Storage | `:key` | Secure token | | `:service` | Name of the service | +### service_delete_prefixed.active_storage + +| Key | Value | +| ------------ | ------------------- | +| `:prefix` | Key prefix | +| `:service` | Name of the service | + ### service_exist.active_storage | Key | Value | diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 43a7de88b0..b4d90d31de 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -1,6 +1,5 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - Using Rails for API-only Applications ===================================== @@ -414,8 +413,10 @@ Some common modules you might want to add: - `AbstractController::Translation`: Support for the `l` and `t` localization and translation methods. -- `ActionController::HttpAuthentication::Basic` (or `Digest` or `Token`): Support - for basic, digest or token HTTP authentication. +- Support for basic, digest or token HTTP authentication: + * `ActionController::HttpAuthentication::Basic::ControllerMethods`, + * `ActionController::HttpAuthentication::Digest::ControllerMethods`, + * `ActionController::HttpAuthentication::Token::ControllerMethods` - `ActionView::Layouts`: Support for layouts when rendering. - `ActionController::MimeResponds`: Support for `respond_to`. - `ActionController::Cookies`: Support for `cookies`, which includes diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 805b0f0d62..618896d458 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -22,8 +22,7 @@ The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages and pre-processors such as CoffeeScript, Sass and ERB. It allows assets in your application to be automatically combined with assets -from other gems. For example, jquery-rails includes a copy of jquery.js -and enables AJAX features in Rails. +from other gems. The asset pipeline is implemented by the [sprockets-rails](https://github.com/rails/sprockets-rails) gem, @@ -35,7 +34,7 @@ rails new appname --skip-sprockets ``` Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier` -gems to your Gemfile, which are used by Sprockets for asset compression: +gems to your `Gemfile`, which are used by Sprockets for asset compression: ```ruby gem 'sass-rails' @@ -44,8 +43,8 @@ gem 'coffee-rails' ``` Using the `--skip-sprockets` option will prevent Rails from adding -them to your Gemfile, so if you later want to enable -the asset pipeline you will have to add those gems to your Gemfile. Also, +them to your `Gemfile`, so if you later want to enable +the asset pipeline you will have to add those gems to your `Gemfile`. Also, creating an application with the `--skip-sprockets` option will generate a slightly different `config/application.rb` file, with a require statement for the sprockets railtie that is commented-out. You will have to remove @@ -850,7 +849,7 @@ This mode uses more memory, performs more poorly than the default and is not recommended. If you are deploying a production application to a system without any -pre-existing JavaScript runtimes, you may want to add one to your Gemfile: +pre-existing JavaScript runtimes, you may want to add one to your `Gemfile`: ```ruby group :production do @@ -1205,10 +1204,10 @@ Adding Assets to Your Gems Assets can also come from external sources in the form of gems. -A good example of this is the `jquery-rails` gem which comes with Rails as the -standard JavaScript library gem. This gem contains an engine class which -inherits from `Rails::Engine`. By doing this, Rails is informed that the -directory for this gem may contain assets and the `app/assets`, `lib/assets` and +A good example of this is the `jquery-rails` gem. +This gem contains an engine class which inherits from `Rails::Engine`. +By doing this, Rails is informed that the directory for this +gem may contain assets and the `app/assets`, `lib/assets` and `vendor/assets` directories of this engine are added to the search path of Sprockets. @@ -1244,11 +1243,7 @@ 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 will be avoiding duplicate JavaScript files. Since jQuery is the default -JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js` -into `app/assets` and it will be included automatically. - -The third is updating the various environment files with the correct default +Next is updating the various environment files with the correct default options. In `application.rb`: diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 9616647f15..f895cadea5 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -572,40 +572,32 @@ class Book < ApplicationRecord end ``` -This declaration needs to be backed up by the proper foreign key declaration on the books table: +This declaration needs to be backed up by a corresponding foreign key column in the books table. For a brand new table, the migration might look something like this: ```ruby class CreateBooks < ActiveRecord::Migration[5.0] def change create_table :books do |t| - t.datetime :published_at - t.string :book_number - t.integer :author_id + t.datetime :published_at + t.string :book_number + t.references :author end end end ``` -If you create an association some time after you build the underlying model, you need to remember to create an `add_column` migration to provide the necessary foreign key. - -It's a good practice to add an index on the foreign key to improve queries -performance and a foreign key constraint to ensure referential data integrity: +Whereas for an existing table, it might look like this: ```ruby -class CreateBooks < ActiveRecord::Migration[5.0] +class AddAuthorToBooks < ActiveRecord::Migration[5.0] def change - create_table :books do |t| - t.datetime :published_at - t.string :book_number - t.integer :author_id - end - - add_index :books, :author_id - add_foreign_key :books, :authors + add_reference :books, :author end end ``` +NOTE: If you wish to [enforce referential integrity at the database level](/active_record_migrations.html#foreign-keys), add the `foreign_key: true` option to the ‘reference’ column declarations above. + #### Creating Join Tables for `has_and_belongs_to_many` Associations If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical book of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering. @@ -735,12 +727,9 @@ a.first_name = 'David' a.first_name == b.author.first_name # => true ``` -Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain any of the following options: +Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain a scope or any of the following options: -* `:conditions` * `:through` -* `:polymorphic` -* `:class_name` * `:foreign_key` For example, consider the following model declarations: @@ -787,12 +776,6 @@ a.first_name = 'David' a.first_name == b.writer.first_name # => true ``` -There are a few limitations to `:inverse_of` support: - -* They do not work with `:through` associations. -* They do not work with `:polymorphic` associations. -* They do not work with `:as` associations. - Detailed Association Reference ------------------------------ @@ -804,7 +787,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d #### Methods Added by `belongs_to` -When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association: +When you declare a `belongs_to` association, the declaring class automatically gains 6 methods related to the association: * `association` * `association=(associate)` @@ -1012,7 +995,7 @@ When we execute `@user.todos.create` then the `@todo` record will have its ##### `:inverse_of` -The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options. +The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. ```ruby class Author < ApplicationRecord @@ -1082,7 +1065,7 @@ You can use any of the standard [querying methods](active_record_querying.html) The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class book < ApplicationRecord +class Book < ApplicationRecord belongs_to :author, -> { where active: true } end ``` @@ -1155,7 +1138,7 @@ The `has_one` association creates a one-to-one match with another model. In data #### Methods Added by `has_one` -When you declare a `has_one` association, the declaring class automatically gains five methods related to the association: +When you declare a `has_one` association, the declaring class automatically gains 6 methods related to the association: * `association` * `association=(associate)` @@ -1299,7 +1282,7 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to ##### `:inverse_of` -The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options. +The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. ```ruby class Supplier < ApplicationRecord @@ -1428,7 +1411,7 @@ The `has_many` association creates a one-to-many relationship with another model #### Methods Added by `has_many` -When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association: +When you declare a `has_many` association, the declaring class automatically gains 17 methods related to the association: * `collection` * `collection<<(object, ...)` @@ -1561,7 +1544,8 @@ The `collection.size` method returns the number of objects in the collection. ##### `collection.find(...)` -The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. +The `collection.find` method finds objects within the collection. It uses the same syntax and options as +[`ActiveRecord::Base.find`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find). ```ruby @available_book = @author.books.find(1) @@ -1693,7 +1677,7 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to ##### `:inverse_of` -The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options. +The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. ```ruby class Author < ApplicationRecord @@ -1960,7 +1944,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi #### Methods Added by `has_and_belongs_to_many` -When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association: +When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 17 methods related to the association: * `collection` * `collection<<(object, ...)` @@ -2091,7 +2075,8 @@ The `collection.size` method returns the number of objects in the collection. ##### `collection.find(...)` -The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. It also adds the additional condition that the object must be in the collection. +The `collection.find` method finds objects within the collection. It uses the same syntax and options as +[`ActiveRecord::Base.find`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find). ```ruby @assembly = @part.assemblies.find(1) @@ -2099,7 +2084,7 @@ The `collection.find` method finds objects within the collection. It uses the sa ##### `collection.where(...)` -The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed. It also adds the additional condition that the object must be in the collection. +The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed. ```ruby @new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago) diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index dea87a18f8..6d30910ce8 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -8,7 +8,7 @@ This guide documents how constant autoloading and reloading works. After reading this guide, you will know: * Key aspects of Ruby constants -* What is `autoload_paths` +* What are the `autoload_paths` and how does eager loading work in production? * How constant autoloading works * What is `require_dependency` * How constant reloading works @@ -430,8 +430,8 @@ if `House` is still unknown when `app/models/beach_house.rb` is being eager loaded, Rails autoloads it. -autoload_paths --------------- +autoload_paths and eager_load_paths +----------------------------------- As you probably know, when `require` gets a relative file name: @@ -451,7 +451,7 @@ the idea is that when a constant like `Post` is hit and missing, if there's a `post.rb` file for example in `app/models` Rails is going to find it, evaluate it, and have `Post` defined as a side-effect. -Alright, Rails has a collection of directories similar to `$LOAD_PATH` in which +All right, Rails has a collection of directories similar to `$LOAD_PATH` in which to look up `post.rb`. That collection is called `autoload_paths` and by default it contains: @@ -465,17 +465,22 @@ default it contains: * The directory `test/mailers/previews`. -Also, this collection is configurable via `config.autoload_paths`. For example, -`lib` was in the list years ago, but no longer is. An application can opt-in -by adding this to `config/application.rb`: +`eager_load_paths` is initially the `app` paths above -```ruby -config.autoload_paths << "#{Rails.root}/lib" -``` +How files are autoloaded depends on `eager_load` and `cache_classes` config settings which typically vary in development, production, and test modes: + + * In **development**, you want quicker startup with incremental loading of application code. So `eager_load` should be set to `false`, and Rails will autoload files as needed (see [Autoloading Algorithms](#autoloading-algorithms) below) -- and then reload them when they change (see [Constant Reloading](#constant-reloading) below). + * In **production**, however you want consistency and thread-safety and can live with a longer boot time. So `eager_load` is set to `true`, and then during boot (before the app is ready to receive requests) Rails loads all files in the `eager_load_paths` and then turns off auto loading (NB: autoloading may be needed during eager loading). Not autoloading after boot is a `good thing`, as autoloading can cause the app to be have thread-safety problems. + * In **test**, for speed of execution (of individual tests) `eager_load` is `false`, so Rails follows development behaviour. + +What is described above are the defaults with a newly generated Rails app. There are multiple ways this can be configured differently (see [Configuring Rails Applications](configuring.html#rails-general-configuration). +). But using `autoload_paths` on its own in the past (before Rails 5) developers might configure `autoload_paths` to add in extra locations (e.g. `lib` which used to be an autoload path list years ago, but no longer is). However this is now discouraged for most purposes, as it is likely to lead to production-only errors. It is possible to add new locations to both `config.eager_load_paths` and `config.autoload_paths` but use at your own risk. + +See also ([Autoloading in the Test Environment](#autoloading-in-the-test-environment). `config.autoload_paths` is not changeable from environment-specific configuration files. -The value of `autoload_paths` can be inspected. In a just generated application +The value of `autoload_paths` can be inspected. In a just-generated application it is (edited): ``` @@ -1329,3 +1334,17 @@ class C < BasicObject end end ``` + +### Autoloading in the Test Environment + +When configuring the `test` environment for autoloading you might consider multiple factors. + +For example it might be worth running your tests with an identical setup to production (`config.eager_load = true`, `config.cache_classes = true`) in order to catch any problems before they hit production (this is compensation for the lack of dev-prod parity). However this will slow down the boot time for individual tests on a dev machine (and is not immediately compatible with spring see below). So one possibility is to do this on a +[CI](https://en.wikipedia.org/wiki/Continuous_integration) machine only (which should run without spring). + +On a development machine you can then have your tests running with whatever is fastest (ideally `config.eager_load = false`). + +With the [Spring](https://github.com/rails/spring) pre-loader (included with new Rails apps), you ideally keep `config.eager_load = false` as per development. Sometimes you may end up with a hybrid configuration (`config.eager_load = true`, `config.cache_classes = true` AND `config.enable_dependency_loading = true`), see [spring issue](https://github.com/rails/spring/issues/519#issuecomment-348324369). However it might be simpler to keep the same configuration as development, and work out whatever it is that is causing autoloading to fail (perhaps by the results of your CI test results). + +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). diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 96650b5be9..cd9f4b4a68 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -32,7 +32,7 @@ Basic Caching This is an introduction to three types of caching techniques: page, action and fragment caching. By default Rails provides fragment caching. In order to use page and action caching you will need to add `actionpack-page_caching` and -`actionpack-action_caching` to your Gemfile. +`actionpack-action_caching` to your `Gemfile`. By default, caching is only enabled in your production environment. To play around with caching locally you'll want to enable caching in your local @@ -100,9 +100,9 @@ called key-based expiration. Cache fragments will also be expired when the view fragment changes (e.g., the HTML in the view changes). The string of characters at the end of the key is a -template tree digest. It is an MD5 hash computed based on the contents of the -view fragment you are caching. If you change the view fragment, the MD5 hash -will change, expiring the existing file. +template tree digest. It is a hash digest computed based on the contents of the +view fragment you are caching. If you change the view fragment, the digest will +change, expiring the existing file. TIP: Cache stores like Memcached will automatically delete old cache files. @@ -366,9 +366,9 @@ There are some common options used by all cache implementations. These can be pa * `: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. -* `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network. +* `:compress` - Enabled by default. Compresses cache entries so more data can be stored in the same memory footprint, leading to fewer cache evictions and higher hit rates. -* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. +* `: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. @@ -444,6 +444,53 @@ The `write` and `fetch` methods on this cache accept two additional options that config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" ``` +### ActiveSupport::Cache::RedisCacheStore + +The Redis cache store takes advantage of Redis support for least-recently-used +and least-frequently-used key eviction when it reaches max memory, allowing it +to behave much like a Memcached cache server. + +Deployment note: Redis doesn't expire keys by default, so take care to use a +dedicated Redis cache server. Don't fill up your persistent-Redis server with +volatile cache data! Read the +[Redis cache server setup guide](https://redis.io/topics/lru-cache) in detail. + +For an all-cache Redis server, set `maxmemory-policy` to an `allkeys` policy. +Redis 4+ support least-frequently-used (`allkeys-lfu`) eviction, an excellent +default choice. Redis 3 and earlier should use `allkeys-lru` for +least-recently-used eviction. + +Set cache read and write timeouts relatively low. Regenerating a cached value +is often faster than waiting more than a second to retrieve it. Both read and +write timeouts default to 1 second, but may be set lower if your network is +consistently low latency. + +Cache reads and writes never raise exceptions. They just return `nil` instead, +behaving as if there was nothing in the cache. To gauge whether your cache is +hitting exceptions, you may provide an `error_handler` to report to an +exception gathering service. It must accept three keyword arguments: `method`, +the cache store method that was originally called; `returning`, the value that +was returned to the user, typically `nil`; and `exception`, the exception that +was rescued. + +Putting it all together, a production Redis cache store may look something +like this: + +```ruby +cache_servers = %w[ "redis://cache-01:6379/0", "redis://cache-02:6379/0", … ], +config.cache_store = :redis_cache_store, url: cache_servers, + + connect_timeout: 30, # Defaults to 20 seconds + read_timeout: 0.2, # Defaults to 1 second + write_timeout: 0.2, # Defaults to 1 second + + error_handler: -> (method:, returning:, exception:) { + # Report errors to Sentry as warnings + Raven.capture_exception exception, level: 'warning", + tags: { method: method, returning: returning } + } +``` + ### ActiveSupport::Cache::NullStore This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 2d03f0a61e..b0f39e7ab5 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -62,12 +62,10 @@ These configuration methods are to be called on a `Rails::Railtie` object, such * `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is `false`, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array. -* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. +* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. It is no longer recommended to adjust this. See [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html#autoload-paths-and-eager-load-paths) * `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to `false` in development mode, and `true` in test and production modes. -* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`. - * `config.beginning_of_week` sets the default beginning of week for the application. Accepts a valid week day symbol (e.g. `:monday`). @@ -202,6 +200,7 @@ The full set of methods that can be used in this block are as follows: * `force_plural` allows pluralized model names. Defaults to `false`. * `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. @@ -322,6 +321,10 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table. +* `config.active_record.internal_metadata_table_name` lets you set a string to be used as the name of the internal metadata table. + +* `config.active_record.protected_environments` lets you set an array of names of environments where destructive actions should be prohibited. + * `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to `true` (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. * `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`. @@ -399,7 +402,7 @@ by adding the following to your `application.rb` file: The schema dumper adds one additional configuration option: -* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. This setting is ignored unless `config.active_record.schema_format == :ruby`. +* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. ### Configuring Action Controller @@ -459,7 +462,10 @@ The schema dumper adds one additional configuration option: config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', - 'X-Content-Type-Options' => 'nosniff' + 'X-Content-Type-Options' => 'nosniff', + 'X-Download-Options' => 'noopen', + 'X-Permitted-Cross-Domain-Policies' => 'none', + 'Referrer-Policy' => 'strict-origin-when-cross-origin' } ``` @@ -534,6 +540,8 @@ Defaults to `'signed cookie'`. `config.action_view` includes a small number of configuration settings: +* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`. + * `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is ```ruby @@ -578,6 +586,8 @@ Defaults to `'signed cookie'`. * `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`. +* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `true`. + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: @@ -666,6 +676,8 @@ There are a few configuration options available in Active Support: * `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. +* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. Defaults to false. + * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. * `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations. @@ -740,6 +752,41 @@ main application. You can set this as nil to not mount Action Cable as part of your normal Rails server. + +### Configuring Active Storage + +`config.active_storage` provides the following configuration options: + +* `config.active_storage.analyzers` accepts an array of classes indicating the analyzers available for Active Storage blobs. The default is `[ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer]`. The former can extract width and height of an image blob; the latter can extract width, height, duration, angle, and aspect ratio of a video blob. + +* `config.active_storage.previewers` accepts an array of classes indicating the image previewers available in Active Storage blobs. The default is `[ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer]`. The former can generate a thumbnail from the first page of a PDF blob; the latter from the relevant frame of a video blob. + +* `config.active_storage.paths` accepts a hash of options indicating the locations of previewer/analyzer commands. The default is `{}`, meaning the commands will be looked for in the default path. Can include any of these options: + * `:ffprobe` - The location of the ffprobe executable. + * `:mutool` - The location of the mutool executable. + * `:ffmpeg` - The location of the ffmpeg executable. + + ```ruby + config.active_storage.paths[:ffprobe] = '/usr/local/bin/ffprobe' + ``` + +* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop)`. + +* `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html +text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml)`. + +* `config.active_storage.queue` can be used to set the name of the Active Job queue used to perform jobs like analyzing the content of a blob or purging a blog. + + ```ruby + config.active_job.queue = :low_priority + ``` + +* `config.active_storage.logger` can be used to set the logger used by Active Storage. Accepts a logger conforming to the interface of Log4r or the default Ruby Logger class. + + ```ruby + config.active_job.logger = ActiveSupport::Logger.new(STDOUT) + ``` + ### 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`. @@ -1003,11 +1050,11 @@ Deploying your application using a reverse proxy has definite advantages over tr Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers. -One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy. +One such application server you can use is [Unicorn](https://bogomips.org/unicorn/) to run behind a reverse proxy. In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. -You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it. +You can find more information in the [Unicorn readme](https://bogomips.org/unicorn/README.html) and understand the [philosophy](https://bogomips.org/unicorn/PHILOSOPHY.html) behind it. Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include: @@ -1035,7 +1082,7 @@ server { } ``` -Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. +Be sure to read the [NGINX documentation](https://nginx.org/en/docs/) for the most up-to-date information. Rails Environment Settings @@ -1057,7 +1104,7 @@ After loading the framework and any gems in your application, Rails turns to loa NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down. -TIP: If you have any ordering dependency in your initializers, you can control the load order through naming. Initializer files are loaded in alphabetical order by their path. For example, `01_critical.rb` will be loaded before `02_normal.rb`. +TIP: While Rails supports numbering of initializer file names for load ordering purposes, a better technique is to place any code that need to load in a specific order within the same file. This reduces file name churn, makes dependencies more explicit, and can help surface new concepts within your application. Initialization events --------------------- diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 7424818757..c1668f989b 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -84,7 +84,9 @@ discussions new features require. Helping to Resolve Existing Issues ---------------------------------- -As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: +As a next step beyond reporting issues, you can help the core team resolve existing ones by providing feedback about them. If you are new to Rails core development, that might be a great way to walk your first steps, you'll get familiar with the code base and the processes. + +If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: ### Verifying Bug Reports @@ -132,7 +134,8 @@ 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, open a pull request to [Rails](https://github.com/rails/rails) on GitHub. +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. 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). diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 59205ee465..5cddf79eeb 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -72,7 +72,7 @@ url: active_support_core_extensions.html description: This guide documents the Ruby core extensions defined in Active Support. - - name: Rails Internationalization API + name: Rails Internationalization (I18n) API url: i18n.html description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on. - @@ -84,6 +84,10 @@ url: active_job_basics.html description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs. - + name: Active Storage Overview + url: active_storage_overview.html + description: This guide covers how to attach files to your Active Record models. + - name: Testing Rails Applications url: testing.html description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to Integration Testing. Enjoy. @@ -104,7 +108,7 @@ url: command_line.html description: This guide covers the command line tools provided by Rails. - - name: Asset Pipeline + name: The Asset Pipeline url: asset_pipeline.html description: This guide documents the asset pipeline. - @@ -151,7 +155,7 @@ url: rails_on_rack.html description: This guide covers Rails integration with Rack and interfacing with other Rack components. - - name: Creating and Customizing Rails Generators + name: Creating and Customizing Rails Generators & Templates url: generators.html description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator). - @@ -159,6 +163,11 @@ url: engines.html description: This guide explains how to write a mountable engine. work_in_progress: true + - + name: Threading and Code Execution in Rails + url: threading_and_code_execution.html + 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 documents: @@ -178,7 +187,7 @@ name: Maintenance Policy documents: - - name: Maintenance Policy + name: Maintenance Policy for Ruby on Rails url: maintenance_policy.html description: What versions of Ruby on Rails are currently supported, and when to expect new versions. - @@ -189,6 +198,10 @@ 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 + url: 5_2_release_notes.html + description: Release notes for Rails 5.2. + - name: Ruby on Rails 5.1 Release Notes url: 5_1_release_notes.html description: Release notes for Rails 5.1. diff --git a/guides/source/engines.md b/guides/source/engines.md index b226eac347..8d81296fa5 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -921,7 +921,7 @@ engine: mattr_accessor :author_class ``` -This method works like its brothers, `attr_accessor` and `cattr_accessor`, but +This method works like its siblings, `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.author_class`. @@ -982,7 +982,7 @@ Blorgh.author_class = "User" WARNING: It's very important here to use the `String` version of the class, rather than the class itself. If you were to use the class, Rails would attempt to load that class and then reference the related table. This could lead to -problems if the table wasn't already existing. Therefore, a `String` should be +problems if the table didn't already exist. Therefore, a `String` should be used and then converted to a class using `constantize` in the engine later on. Go ahead and try to create a new article. You will see that it works exactly in the @@ -1514,14 +1514,14 @@ To hook into the initialization process of one of the following classes use the ## Configuration hooks -These are the available configuration hooks. They do not hook into any particular framework, instead they run in context of the entire application. +These are the available configuration hooks. They do not hook into any particular framework, but instead they run in context of the entire application. -| Hook | Use Case | -| ---------------------- | ------------------------------------------------------------------------------------- | -| `before_configuration` | First configurable block to run. Called before any initializers are run. | -| `before_initialize` | Second configurable block to run. Called before frameworks initialize. | -| `before_eager_load` | Third configurable block to run. Does not run if `config.cache_classes` set to false. | -| `after_initialize` | Last configurable block to run. Called after frameworks initialize. | +| Hook | Use Case | +| ---------------------- | ---------------------------------------------------------------------------------- | +| `before_configuration` | First configurable block to run. Called before any initializers are run. | +| `before_initialize` | Second configurable block to run. Called before frameworks initialize. | +| `before_eager_load` | Third configurable block to run. Does not run if `config.eager_load` set to false. | +| `after_initialize` | Last configurable block to run. Called after frameworks initialize. | ### Example diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 4ce67df93a..53c567727f 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -1,7 +1,7 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** -Form Helpers -============ +Action View Form Helpers +======================== Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use. @@ -920,7 +920,7 @@ When an association accepts nested attributes `fields_for` renders its block onc ```ruby def new @person = Person.new - 2.times { @person.addresses.build} + 2.times { @person.addresses.build } end ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index b007baea87..6cf99a7e5c 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -462,8 +462,7 @@ You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view available, Rails will raise an exception. -In the above image, the bottom line has been truncated. Let's see what the full -error message looks like: +Let's look at the full error message again: >ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot. diff --git a/guides/source/i18n.md b/guides/source/i18n.md index e6aa6181cc..2b545e6b82 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -977,7 +977,7 @@ en: ``` NOTE: In order to use this helper, you need to install [DynamicForm](https://github.com/joelmoss/dynamic_form) -gem by adding this line to your Gemfile: `gem 'dynamic_form'`. +gem by adding this line to your `Gemfile`: `gem 'dynamic_form'`. ### Translations for Action Mailer E-Mail Subjects diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 1541ea38cd..c4f1df487b 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -93,7 +93,7 @@ require 'bundler/setup' # Set up gems listed in the Gemfile. In a standard Rails application, there's a `Gemfile` which declares all dependencies of the application. `config/boot.rb` sets -`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile +`ENV['BUNDLE_GEMFILE']` to the location of this file. If the `Gemfile` exists, then `bundler/setup` is required. The require is used by Bundler to configure the load path for your Gemfile's dependencies. diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 334595e4d2..3981199e95 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -99,9 +99,9 @@ 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. </p> <p> - You may also find incomplete content, or stuff that is not up to date. + 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', 'http://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. @@ -111,7 +111,7 @@ <%= link_to 'open an issue', 'https://github.com/rails/rails/issues' %>. </p> <p>And last but not least, any kind of discussion regarding Ruby on Rails - documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>. + documentation is very welcome on the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>. </p> </div> </div> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index b9b327252f..15345c94b7 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -97,7 +97,7 @@ If we want to display the properties of all the books in our view, we can do so <%= link_to "New book", new_book_path %> ``` -NOTE: The actual rendering is done by subclasses of `ActionView::TemplateHandlers`. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. Beginning with Rails 2, the standard extensions are `.erb` for ERB (HTML with embedded Ruby), and `.builder` for Builder (XML generator). +NOTE: The actual rendering is done by nested classes of the module [`ActionView::Template::Handlers`](http://api.rubyonrails.org/classes/ActionView/Template/Handlers.html). This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. ### Using `render` @@ -285,7 +285,7 @@ the response. Using `:plain` or `:html` might be more appropriate most of the time. NOTE: Unless overridden, your response returned from this render option will be -`text/html`, as that is the default content type of Action Dispatch response. +`text/plain`, as that is the default content type of Action Dispatch response. #### Options for `render` @@ -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 member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option. +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. 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/rails_on_rack.md b/guides/source/rails_on_rack.md index aa1476ecc0..1627205b7b 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -122,9 +122,11 @@ use ActiveRecord::Migration::CheckPending use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash +use ActionDispatch::ContentSecurityPolicy::Middleware use Rack::Head use Rack::ConditionalGet use Rack::ETag +use Rack::TempfileReaper run MyApp::Application.routes ``` @@ -249,7 +251,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`Rails::Rack::Logger`** -* Notifies the logs that the request has began. After request is complete, flushes all the logs. +* Notifies the logs that the request has begun. After the request is complete, flushes all the logs. **`ActionDispatch::ShowExceptions`** @@ -283,18 +285,26 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value. +**`ActionDispatch::ContentSecurityPolicy::Middleware`** + +* Provides a DSL to configure a Content-Security-Policy header. + **`Rack::Head`** * Converts HEAD requests to `GET` requests and serves them as so. **`Rack::ConditionalGet`** -* Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed. +* Adds support for "Conditional `GET`" so that server responds with nothing if the page wasn't changed. **`Rack::ETag`** * Adds ETag header on all String bodies. ETags are used to validate cache. +**`Rack::TempfileReaper`** + +* Cleans up tempfiles used to buffer multipart requests. + TIP: It's possible to use any of the above middlewares in your custom Rack stack. Resources diff --git a/guides/source/routing.md b/guides/source/routing.md index 638f77be13..efc0e32b56 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -852,6 +852,49 @@ You can specify unicode character routes directly. For example: get 'こんにちは', to: 'welcome#index' ``` +### Direct routes + +You can create custom URL helpers directly. For example: + +```ruby +direct :homepage do + "http://www.rubyonrails.org" +end + +# >> homepage_url +# => "http://www.rubyonrails.org" +``` + +The return value of the block must be a valid argument for the `url_for` method. So, you can pass a valid string URL, Hash, Array, an Active Model instance, or an Active Model class. + +```ruby +direct :commentable do |model| + [ model, anchor: model.dom_id ] +end + +direct :main do + { controller: 'pages', action: 'index', subdomain: 'www' } +end +``` + +### Using `resolve` + +The `resolve` method allows customizing polymorphic mapping of models. For example: + +``` ruby +resource :basket + +resolve("Basket") { [:basket] } +``` + +``` erb +<%= form_for @basket do |form| %> + <!-- basket form --> +<% end %> +``` + +This will generate the singular URL `/basket` instead of the usual `/baskets/:id`. + Customizing Resourceful Routes ------------------------------ diff --git a/guides/source/security.md b/guides/source/security.md index cfa777d433..28ddbdc26a 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -1,7 +1,7 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** -Ruby on Rails Security Guide -============================ +Securing Rails Applications +=========================== This manual describes common security problems in web applications and how to avoid them with Rails. @@ -52,7 +52,7 @@ User.find(session[:user_id]) NOTE: _The session ID is a 32-character random hex string._ -The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs. +The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32 CryptoAPI) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs. ### Session Hijacking @@ -192,9 +192,9 @@ rotations going at any one time. For more details on key rotation with encrypted and signed messages as well as the various options the `rotate` method accepts, please refer to the -[MessageEncryptor API](api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html) +[MessageEncryptor API](http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html) and -[MessageVerifier API](api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html) +[MessageVerifier API](http://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html) documentation. ### Replay Attacks for CookieStore Sessions @@ -474,7 +474,7 @@ The common admin interface works like this: it's located at www.example.com/admi * Does the admin really have to access the interface from everywhere in the world? Think about _limiting the login to a bunch of source IP addresses_. Examine request.remote_ip to find out about the user's IP address. This is not bullet-proof, but a great barrier. Remember that there might be a proxy in use, though. -* _Put the admin interface to a special sub-domain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa. +* _Put the admin interface to a special subdomain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa. User Management --------------- @@ -551,7 +551,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS: * make the elements very small or color them the same as the background of the page * leave the fields displayed, but tell humans to leave them blank -The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too. +The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html): @@ -573,18 +573,6 @@ config.filter_parameters << :password NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`. -### Good Passwords - -INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._ - -Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are: - -password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey. - -It is interesting that only 4% of these passwords were dictionary words and the great majority is actually alphanumeric. However, password cracker dictionaries contain a large number of today's passwords, and they try out all kinds of (alphanumerical) combinations. If an attacker knows your user name and you use a weak password, your account will be easily cracked. - -A good password is a long alphanumeric combination of mixed cases. As this is quite hard to remember, it is advisable to enter only the _first letters of a sentence that you can easily remember_. For example "The quick brown fox jumps over the lazy dog" will be "Tqbfjotld". Note that this is just an example, you should not use well known phrases like these, as they might appear in cracker dictionaries, too. - ### Regular Expressions INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._ @@ -1070,7 +1058,10 @@ Every HTTP response from your Rails application receives the following default s config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', - 'X-Content-Type-Options' => 'nosniff' + 'X-Content-Type-Options' => 'nosniff', + 'X-Download-Options' => 'noopen', + 'X-Permitted-Cross-Domain-Policies' => 'none', + 'Referrer-Policy' => 'strict-origin-when-cross-origin' } ``` diff --git a/guides/source/testing.md b/guides/source/testing.md index c5b2a694e7..af6127f4a5 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1,7 +1,7 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** -A Guide to Testing Rails Applications -===================================== +Testing Rails Applications +========================== This guide covers built-in mechanisms in Rails for testing your application. @@ -319,6 +319,8 @@ specify to make your test failure messages clearer. | `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.| | `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| | `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| +| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.| +| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| | `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| | `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.| @@ -645,7 +647,7 @@ system tests should live. If you want to change the default settings you can change what the system tests are "driven by". Say you want to change the driver from Selenium to -Poltergeist. First add the `poltergeist` gem to your Gemfile. Then in your +Poltergeist. First add the `poltergeist` gem to your `Gemfile`. Then in your `application_system_test_case.rb` file do the following: ```ruby @@ -671,6 +673,17 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase end ``` +If you want to use a headless browser, you could use Headless Chrome or Headless Firefox by adding +`headless_chrome` or `headless_firefox` in the `:using` argument. + +```ruby +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome +end +``` + If your Capybara configuration requires more setup than provided by Rails, this additional configuration could be added into the `application_system_test_case.rb` file. @@ -765,9 +778,37 @@ Then the test will fill in the title and body of the article with the specified text. Once the fields are filled in, "Create Article" is clicked on which will send a POST request to create the new article in the database. -We will be redirected back to the the articles index page and there we assert +We will be redirected back to the articles index page and there we assert that the text from the new article's title is on the articles index page. +#### Testing for multiple screen sizes +If you want to test for mobile sizes on top of testing for desktop, +you can create another class that inherits from SystemTestCase and use in your +test suite. In this example a file called `mobile_system_test_case.rb` is created +in the `/test` directory with the following configuration. + +```ruby +require "test_helper" + +class MobileSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [375, 667] +end +``` +To use this configuration, create a test inside `test/system` that inherits from `MobileSystemTestCase`. +Now you can test your app using multiple different configurations. + +```ruby +require "mobile_system_test_case" + +class PostsTest < MobileSystemTestCase + + test "visiting the index" do + visit posts_url + assert_selector "h1", text: "Posts" + end +end +``` + #### Taking it further The beauty of system testing is that it is similar to integration testing in @@ -1494,7 +1535,7 @@ Testing Jobs ------------ Since your custom jobs can be queued at different levels inside your application, -you'll need to test both, the jobs themselves (their behavior when they get enqueued) +you'll need to test both the jobs themselves (their behavior when they get enqueued) and that other entities correctly enqueue them. ### A Basic Test Case diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md new file mode 100644 index 0000000000..e4febc7507 --- /dev/null +++ b/guides/source/threading_and_code_execution.md @@ -0,0 +1,324 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Threading and Code Execution in Rails +===================================== + +After reading this guide, you will know: + +* What code Rails will automatically execute concurrently +* How to integrate manual concurrency with Rails internals +* How to wrap all application code +* How to affect application reloading + +-------------------------------------------------------------------------------- + +Automatic Concurrency +--------------------- + +Rails automatically allows various operations to be performed at the same time. + +When using a threaded web server, such as the default Puma, multiple HTTP +requests will be served simultaneously, with each request provided its own +controller instance. + +Threaded Active Job adapters, including the built-in Async, will likewise +execute several jobs at the same time. Action Cable channels are managed this +way too. + +These mechanisms all involve multiple threads, each managing work for a unique +instance of some object (controller, job, channel), while sharing the global +process space (such as classes and their configurations, and global variables). +As long as your code doesn't modify any of those shared things, it can mostly +ignore that other threads exist. + +The rest of this guide describes the mechanisms Rails uses to make it "mostly +ignorable", and how extensions and applications with special needs can use them. + +Executor +-------- + +The Rails Executor separates application code from framework code: any time the +framework invokes code you've written in your application, it will be wrapped by +the Executor. + +The Executor consists of two callbacks: `to_run` and `to_complete`. The Run +callback is called before the application code, and the Complete callback is +called after. + +### Default callbacks + +In a default Rails application, the Executor callbacks are used to: + +* track which threads are in safe positions for autoloading and reloading +* enable and disable the Active Record query cache +* return acquired Active Record connections to the pool +* constrain internal cache lifetimes + +Prior to Rails 5.0, some of these were handled by separate Rack middleware +classes (such as `ActiveRecord::ConnectionAdapters::ConnectionManagement`), or +directly wrapping code with methods like +`ActiveRecord::Base.connection_pool.with_connection`. The Executor replaces +these with a single more abstract interface. + +### Wrapping application code + +If you're writing a library or component that will invoke application code, you +should wrap it with a call to the executor: + +```ruby +Rails.application.executor.wrap do + # call application code here +end +``` + +TIP: If you repeatedly invoke application code from a long-running process, you +may want to wrap using the Reloader instead. + +Each thread should be wrapped before it runs application code, so if your +application manually delegates work to other threads, such as via `Thread.new` +or Concurrent Ruby features that use thread pools, you should immediately wrap +the block: + +```ruby +Thread.new do + Rails.application.executor.wrap do + # your code here + end +end +``` + +NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures +with an `executor` option. Despite the name, it is unrelated. + +The Executor is safely re-entrant; if it is already active on the current +thread, `wrap` is a no-op. + +If it's impractical to wrap the application code in a block (for +example, the Rack API makes this problematic), you can also use the `run!` / +`complete!` pair: + +```ruby +Thread.new do + execution_context = Rails.application.executor.run! + # your code here +ensure + execution_context.complete! if execution_context +end +``` + +### Concurrency + +The Executor will put the current thread into `running` mode in the Load +Interlock. This operation will block temporarily if another thread is currently +either autoloading a constant or unloading/reloading the application. + +Reloader +-------- + +Like the Executor, the Reloader also wraps application code. If the Executor is +not already active on the current thread, the Reloader will invoke it for you, +so you only need to call one. This also guarantees that everything the Reloader +does, including all its callback invocations, occurs wrapped inside the +Executor. + +```ruby +Rails.application.reloader.wrap do + # call application code here +end +``` + +The Reloader is only suitable where a long-running framework-level process +repeatedly calls into application code, such as for a web server or job queue. +Rails automatically wraps web requests and Active Job workers, so you'll rarely +need to invoke the Reloader for yourself. Always consider whether the Executor +is a better fit for your use case. + +### Callbacks + +Before entering the wrapped block, the Reloader will check whether the running +application needs to be reloaded -- for example, because a model's source file has +been modified. If it determines a reload is required, it will wait until it's +safe, and then do so, before continuing. When the application is configured to +always reload regardless of whether any changes are detected, the reload is +instead performed at the end of the block. + +The Reloader also provides `to_run` and `to_complete` callbacks; they are +invoked at the same points as those of the Executor, but only when the current +execution has initiated an application reload. When no reload is deemed +necessary, the Reloader will invoke the wrapped block with no other callbacks. + +### Class Unload + +The most significant part of the reloading process is the Class Unload, where +all autoloaded classes are removed, ready to be loaded again. This will occur +immediately before either the Run or Complete callback, depending on the +`reload_classes_only_on_change` setting. + +Often, additional reloading actions need to be performed either just before or +just after the Class Unload, so the Reloader also provides `before_class_unload` +and `after_class_unload` callbacks. + +### Concurrency + +Only long-running "top level" processes should invoke the Reloader, because if +it determines a reload is needed, it will block until all other threads have +completed any Executor invocations. + +If this were to occur in a "child" thread, with a waiting parent inside the +Executor, it would cause an unavoidable deadlock: the reload must occur before +the child thread is executed, but it cannot be safely performed while the parent +thread is mid-execution. Child threads should use the Executor instead. + +Framework Behavior +------------------ + +The Rails framework components use these tools to manage their own concurrency +needs too. + +`ActionDispatch::Executor` and `ActionDispatch::Reloader` are Rack middlewares +that wraps the request with a supplied Executor or Reloader, respectively. They +are automatically included in the default application stack. The Reloader will +ensure any arriving HTTP request is served with a freshly-loaded copy of the +application if any code changes have occurred. + +Active Job also wraps its job executions with the Reloader, loading the latest +code to execute each job as it comes off the queue. + +Action Cable uses the Executor instead: because a Cable connection is linked to +a specific instance of a class, it's not possible to reload for every arriving +websocket message. Only the message handler is wrapped, though; a long-running +Cable connection does not prevent a reload that's triggered by a new incoming +request or job. Instead, Action Cable uses the Reloader's `before_class_unload` +callback to disconnect all its connections. When the client automatically +reconnects, it will be speaking to the new version of the code. + +The above are the entry points to the framework, so they are responsible for +ensuring their respective threads are protected, and deciding whether a reload +is necessary. Other components only need to use the Executor when they spawn +additional threads. + +### Configuration + +The Reloader only checks for file changes when `cache_classes` is false and +`reload_classes_only_on_change` is true (which is the default in the +`development` environment). + +When `cache_classes` is true (in `production`, by default), the Reloader is only +a pass-through to the Executor. + +The Executor always has important work to do, like database connection +management. When `cache_classes` and `eager_load` are both true (`production`), +no autoloading or class reloading will occur, so it does not need the Load +Interlock. If either of those are false (`development`), then the Executor will +use the Load Interlock to ensure constants are only loaded when it is safe. + +Load Interlock +-------------- + +The Load Interlock allows autoloading and reloading to be enabled in a +multi-threaded runtime environment. + +When one thread is performing an autoload by evaluating the class definition +from the appropriate file, it is important no other thread encounters a +reference to the partially-defined constant. + +Similarly, it is only safe to perform an unload/reload when no application code +is in mid-execution: after the reload, the `User` constant, for example, may +point to a different class. Without this rule, a poorly-timed reload would mean +`User.new.class == User`, or even `User == User`, could be false. + +Both of these constraints are addressed by the Load Interlock. It keeps track of +which threads are currently running application code, loading a class, or +unloading autoloaded constants. + +Only one thread may load or unload at a time, and to do either, it must wait +until no other threads are running application code. If a thread is waiting to +perform a load, it doesn't prevent other threads from loading (in fact, they'll +cooperate, and each perform their queued load in turn, before all resuming +running together). + +### `permit_concurrent_loads` + +The Executor automatically acquires a `running` lock for the duration of its +block, and autoload knows when to upgrade to a `load` lock, and switch back to +`running` again afterwards. + +Other blocking operations performed inside the Executor block (which includes +all application code), however, can needlessly retain the `running` lock. If +another thread encounters a constant it must autoload, this can cause a +deadlock. + +For example, assuming `User` is not yet loaded, the following will deadlock: + +```ruby +Rails.application.executor.wrap do + th = Thread.new do + Rails.application.executor.wrap do + User # inner thread waits here; it cannot load + # User while another thread is running + end + end + + th.join # outer thread waits here, holding 'running' lock +end +``` + +To prevent this deadlock, the outer thread can `permit_concurrent_loads`. By +calling this method, the thread guarantees it will not dereference any +possibly-autoloaded constant inside the supplied block. The safest way to meet +that promise is to put it as close as possible to the blocking call: + +```ruby +Rails.application.executor.wrap do + th = Thread.new do + Rails.application.executor.wrap do + User # inner thread can acquire the 'load' lock, + # load User, and continue + end + end + + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + th.join # outer thread waits here, but has no lock + end +end +``` + +Another example, using Concurrent Ruby: + +```ruby +Rails.application.executor.wrap do + futures = 3.times.collect do |i| + Concurrent::Future.execute do + Rails.application.executor.wrap do + # do work here + end + end + end + + values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + futures.collect(&:value) + end +end +``` + + +### ActionDispatch::DebugLocks + +If your application is deadlocking and you think the Load Interlock may be +involved, you can temporarily add the ActionDispatch::DebugLocks middleware to +`config/application.rb`: + +```ruby +config.middleware.insert_before Rack::Sendfile, + ActionDispatch::DebugLocks +``` + +If you then restart the application and re-trigger the deadlock condition, +`/rails/locks` will show a summary of all threads currently known to the +interlock, which lock level they are holding or awaiting, and their current +backtrace. + +Generally a deadlock will be caused by the interlock conflicting with some other +external lock or blocking I/O call. Once you find it, you can wrap it with +`permit_concurrent_loads`. + diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 9bc87e4bf0..51b284ff12 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -1,7 +1,7 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** -A Guide for Upgrading Ruby on Rails -=================================== +Upgrading Ruby on Rails +======================= This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides. @@ -45,7 +45,7 @@ TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterp ### The Update Task Rails provides the `app:update` task (`rake rails:update` on 4.2 and earlier). After updating the Rails version -in the Gemfile, run this task. +in the `Gemfile`, run this task. This will help you with the creation of new files and changes of old files in an interactive session. @@ -65,6 +65,17 @@ 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. +Upgrading from Rails 5.1 to Rails 5.2 +------------------------------------- + +For more information on changes made to Rails 5.2 please see the [release notes](5_2_release_notes.html). + +### Bootsnap + +Rails 5.2 adds bootsnap gem in the [newly generated app's Gemfile](https://github.com/rails/rails/pull/29313). +The `app:update` task sets it up in `boot.rb`. If you want to use it, then add it in the Gemfile, +otherwise change the `boot.rb` to not use bootsnap. + Upgrading from Rails 5.0 to Rails 5.1 ------------------------------------- @@ -72,7 +83,7 @@ For more information on changes made to Rails 5.1 please see the [release notes] ### Top-level `HashWithIndifferentAccess` is soft-deprecated -If your application uses the the top-level `HashWithIndifferentAccess` class, you +If your application uses the top-level `HashWithIndifferentAccess` class, you should slowly move your code to instead use `ActiveSupport::HashWithIndifferentAccess`. It is only soft-deprecated, which means that your code will not break at the @@ -179,7 +190,7 @@ See [#19034](https://github.com/rails/rails/pull/19034) for more details. `assigns` and `assert_template` have been extracted to the `rails-controller-testing` gem. To continue using these methods in your controller tests, add `gem 'rails-controller-testing'` to -your Gemfile. +your `Gemfile`. If you are using Rspec for testing, please see the extra configuration required in the gem's documentation. @@ -212,7 +223,7 @@ true. `ActiveModel::Serializers::Xml` has been extracted from Rails to the `activemodel-serializers-xml` gem. To continue using XML serialization in your application, add `gem 'activemodel-serializers-xml'` -to your Gemfile. +to your `Gemfile`. ### Removed Support for Legacy `mysql` Database Adapter @@ -278,7 +289,7 @@ You can now just call the dependency once with a wildcard. ### `ActionView::Helpers::RecordTagHelper` moved to external gem (record_tag_helper) -`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your Gemfile: +`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your `Gemfile`: ```ruby gem 'record_tag_helper', '~> 1.0' @@ -415,7 +426,7 @@ First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your `Ge ### Responders -`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your Gemfile. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies: +`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your `Gemfile`. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies: ```ruby # app/controllers/users_controller.rb @@ -559,7 +570,7 @@ Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more The documentation for `PermitScrubber` and `TargetScrubber` explains how you can gain complete control over when and how elements should be stripped. -If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your Gemfile: +If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your `Gemfile`: ```ruby gem 'rails-deprecated_sanitizer' @@ -567,7 +578,7 @@ gem 'rails-deprecated_sanitizer' ### Rails DOM Testing -The [`TagAssertions` module](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing). +The [`TagAssertions` module](http://api.rubyonrails.org/v4.1/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing). ### Masked Authenticity Tokens @@ -617,7 +628,7 @@ migration DSL counterpart. The migration procedure is as follows: -1. remove `gem "foreigner"` from the Gemfile. +1. remove `gem "foreigner"` from the `Gemfile`. 2. run `bundle install`. 3. run `bin/rake db:schema:dump`. 4. make sure that `db/schema.rb` contains every foreign key definition with @@ -769,7 +780,7 @@ and has been removed from Rails. If your application currently depends on MultiJSON directly, you have a few options: -1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future +1. Add 'multi_json' to your `Gemfile`. Note that this might cease to work in the future 2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead. @@ -810,7 +821,7 @@ part of the rewrite, the following features have been removed from the encoder: If your application depends on one of these features, you can get them back by adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) -gem to your Gemfile. +gem to your `Gemfile`. #### JSON representation of Time objects @@ -1135,7 +1146,7 @@ full support for the last few changes in the specification. ### Gemfile -Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that +Rails 4.0 removed the `assets` group from `Gemfile`. You'd need to remove that line from your `Gemfile` when upgrading. You should also update your application file (in `config/application.rb`): @@ -1147,7 +1158,7 @@ Bundler.require(*Rails.groups) ### vendor/plugins -Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. ### Active Record @@ -1214,7 +1225,7 @@ end ### Active Resource -Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile. +Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your `Gemfile`. ### Active Model @@ -1414,7 +1425,7 @@ config.active_record.mass_assignment_sanitizer = :strict ### vendor/plugins -Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. ### Active Record diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index b2716c7faa..b9ea4ad47a 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -188,15 +188,20 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out: ```coffeescript $(document).ready -> - $("#new_article").on("ajax:success", (e, data, status, xhr) -> + $("#new_article").on("ajax:success", (event) -> + [data, status, xhr] = event.detail $("#new_article").append xhr.responseText - ).on "ajax:error", (e, xhr, status, error) -> + ).on "ajax:error", (event) -> $("#new_article").append "<p>ERROR</p>" ``` Obviously, you'll want to be a bit more sophisticated than that, but it's a start. +NOTE: As of Rails 5.1 and the new `rails-ujs`, the parameters `data, status, xhr` +have been bundled into `event.detail`. For information about the previously used +`jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax). + #### link_to [`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) @@ -225,7 +230,7 @@ and write some CoffeeScript like this: ```coffeescript $ -> - $("a[data-remote]").on "ajax:success", (e, data, status, xhr) -> + $("a[data-remote]").on "ajax:success", (event) -> alert "The article was deleted." ``` @@ -343,39 +348,6 @@ This generates a form with: <input data-disable-with="Saving..." type="submit"> ``` -Dealing with Ajax events ------------------------- - -Here are the different events that are fired when you deal with elements -that have a `data-remote` attribute: - -NOTE: All handlers bound to these events are always passed the event object as the -first argument. The table below describes the extra parameters passed after the -event argument. For example, if the extra parameters are listed as `xhr, settings`, -then to access them, you would define your handler with `function(event, xhr, settings)`. - -| Event name | Extra parameters | Fired | -|---------------------|------------------|-------------------------------------------------------------| -| `ajax:before` | | Before the whole ajax business, aborts if stopped. | -| `ajax:beforeSend` | xhr, options | Before the request is sent, aborts if stopped. | -| `ajax:send` | xhr | When the request is sent. | -| `ajax:success` | xhr, status, err | After completion, if the response was a success. | -| `ajax:error` | xhr, status, err | After completion, if the response was an error. | -| `ajax:complete` | xhr, status | After the request has been completed, no matter the outcome.| -| `ajax:aborted:file` | elements | If there are non-blank file inputs, aborts if stopped. | - -### Stoppable events - -If you stop `ajax:before` or `ajax:beforeSend` by returning false from the -handler method, the Ajax request will never take place. The `ajax:before` event -is also useful for manipulating form data before serialization. The -`ajax:beforeSend` event is also useful for adding custom request headers. - -If you stop the `ajax:aborted:file` event, the default behavior of allowing the -browser to submit the form via normal means (i.e. non-Ajax submission) will be -canceled and the form will not be submitted at all. This is useful for -implementing your own Ajax file upload workaround. - ### Rails-ujs event handlers Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency. @@ -401,10 +373,26 @@ Example usage: ```html document.body.addEventListener('ajax:success', function(event) { var detail = event.detail; - var data = detail[0], status = detail[1], xhr = detail[2]; + var data = detail[0], status = detail[1], xhr = detail[2]; }) ``` +NOTE: As of Rails 5.1 and the new `rails-ujs`, the parameters `data, status, xhr` +have been bundled into `event.detail`. For information about the previously used +`jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax). + +### Stoppable events + +If you stop `ajax:before` or `ajax:beforeSend` by returning false from the +handler method, the Ajax request will never take place. The `ajax:before` event +can manipulate form data before serialization and the +`ajax:beforeSend` event is useful for adding custom request headers. + +If you stop the `ajax:aborted:file` event, the default behavior of allowing the +browser to submit the form via normal means (i.e. non-Ajax submission) will be +canceled and the form will not be submitted at all. This is useful for +implementing your own Ajax file upload workaround. + Server-Side Concerns -------------------- @@ -504,7 +492,7 @@ 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, +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`. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e69a1231b1..b3b35307e3 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,153 +1,3 @@ -* `rails new` and `rails plugin new` get `Active Storage` by default. - Add ability to skip `Active Storage` with `--skip-active-storage` - and do so automatically when `--skip-active-record` is used. - *bogdanvlviv* -* Gemfile for new apps: upgrade redis-rb from ~> 3.0 to 4.0. - - *Jeremy Daer* - -* Add `mini_magick` to default `Gemfile` as comment. - - *Yoshiyuki Hirano* - -* Derive `secret_key_base` from the app name in development and test environments. - - Spares away needless secret configs. - - *DHH*, *Kasper Timm Hansen* - -* Support multiple versions arguments for `gem` method of Generators. - - *Yoshiyuki Hirano* - -* Add `--skip-yarn` option to the plugin generator. - - *bogdanvlviv* - -* Optimize routes indentation. - - *Yoshiyuki Hirano* - -* Optimize indentation for generator actions. - - *Yoshiyuki Hirano* - -* Skip unused components when running `bin/rails` in Rails plugin. - - *Yoshiyuki Hirano* - -* Add `git_source` to `Gemfile` for plugin generator. - - *Yoshiyuki Hirano* - -* Add `--skip-action-cable` option to the plugin generator. - - *bogdanvlviv* - -* Deprecate support of use `Rails::Application` subclass to start Rails server. - - *Yuji Yaginuma* - -* Add `ruby x.x.x` version to `Gemfile` and create `.ruby-version` - root file containing the current Ruby version when new Rails applications are - created. - - *Alberto Almagro* - -* Support `-` as a platform-agnostic way to run a script from stdin with - `rails runner` - - *Cody Cutrer* - -* Add `bootsnap` to default `Gemfile`. - - *Burke Libbey* - -* Properly expand shortcuts for environment's name running the `console` - and `dbconsole` commands. - - *Robin Dupret* - -* Passing the environment's name as a regular argument to the - `rails dbconsole` and `rails console` commands is deprecated. - The `-e` option should be used instead. - - Previously: - - $ bin/rails dbconsole production - - Now: - - $ bin/rails dbconsole -e production - - *Robin Dupret*, *Kasper Timm Hansen* - -* Allow passing a custom connection name to the `rails dbconsole` - command when using a 3-level database configuration. - - $ bin/rails dbconsole -c replica - - *Robin Dupret*, *Jeremy Daer* - -* Skip unused components when running `bin/rails app:update`. - - If the initial app generation skipped Action Cable, Active Record etc., - the update task honors those skips too. - - *Yuji Yaginuma* - -* Make Rails' test runner work better with minitest plugins. - - By demoting the Rails test runner to just another minitest plugin — - and thereby not eager loading it — we can co-exist much better with - other minitest plugins such as pride and minitest-focus. - - *Kasper Timm Hansen* - -* Load environment file in `dbconsole` command. - - Fixes #29717. - - *Yuji Yaginuma* - -* Add `rails secrets:show` command. - - *Yuji Yaginuma* - -* Allow mounting the same engine several times in different locations. - - Fixes #20204. - - *David Rodríguez* - -* Clear screenshot files in `tmp:clear` task. - - *Yuji Yaginuma* - -* Add `railtie.rb` to the plugin generator - - *Tsukuru Tanimichi* - -* Deprecate `capify!` method in generators and templates. - - *Yuji Yaginuma* - -* Allow irb options to be passed from `rails console` command. - - Fixes #28988. - - *Yuji Yaginuma* - -* Added a shared section to `config/database.yml` that will be loaded for all environments. - - *Pierre Schambacher* - -* Namespace error pages' CSS selectors to stop the styles from bleeding into other pages - when using Turbolinks. - - *Jan Krutisch* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index f9e4444f07..cce00cbc3a 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index ade8cb6a48..a200a1005c 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -174,8 +174,9 @@ module Rails # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= if secret_key_base - ActiveSupport::CachingKeyGenerator.new \ + ActiveSupport::CachingKeyGenerator.new( ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000) + ) else ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token) end @@ -265,7 +266,9 @@ module Rails "action_dispatch.signed_cookie_digest" => config.action_dispatch.signed_cookie_digest, "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.cookies_rotations" => config.action_dispatch.cookies_rotations, + "action_dispatch.content_security_policy" => config.content_security_policy, + "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only ) end end @@ -400,8 +403,9 @@ module Rails secrets.secret_token ||= config.secret_token if secrets.secret_token.present? - ActiveSupport::Deprecation.warn \ + ActiveSupport::Deprecation.warn( "`secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0." + ) end secrets @@ -424,19 +428,53 @@ module Rails if Rails.env.test? || Rails.env.development? Digest::MD5.hexdigest self.class.name else - validate_secret_key_base \ + validate_secret_key_base( ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base + ) end end - # 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 ENV["RAILS_MASTER_KEY"] or from loading - # `config/master.key`. + # 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+. def credentials - @credentials ||= ActiveSupport::EncryptedConfiguration.new \ - config_path: Rails.root.join("config/credentials.yml.enc"), - key_path: Rails.root.join("config/master.key"), - env_key: "RAILS_MASTER_KEY" + @credentials ||= encrypted("config/credentials.yml.enc") + end + + # Shorthand to decrypt any encrypted configurations or files. + # + # For any file added with <tt>bin/rails encrypted:edit</tt> call +read+ to decrypt + # the file with the master key. + # The master key is either stored in +config/master.key+ or <tt>ENV["RAILS_MASTER_KEY"]</tt>. + # + # Rails.application.encrypted("config/mystery_man.txt.enc").read + # # => "We've met before, haven't we?" + # + # It's also possible to interpret encrypted YAML files with +config+. + # + # Rails.application.encrypted("config/credentials.yml.enc").config + # # => { next_guys_line: "I don't think so. Where was it you think we met?" } + # + # Any top-level configs are also accessible directly on the return value: + # + # Rails.application.encrypted("config/credentials.yml.enc").next_guys_line + # # => "I don't think so. Where was it you think we met?" + # + # The files or configs can also be encrypted with a custom key. To decrypt with + # a key in the +ENV+, use: + # + # Rails.application.encrypted("config/special_tokens.yml.enc", env_key: "SPECIAL_TOKENS") + # + # Or to decrypt with a file, that should be version control ignored, relative to +Rails.root+: + # + # Rails.application.encrypted("config/special_tokens.yml.enc", key_path: "config/special_tokens.key") + def encrypted(path, key_path: "config/master.key", env_key: "RAILS_MASTER_KEY") + ActiveSupport::EncryptedConfiguration.new( + config_path: Rails.root.join(path), + key_path: Rails.root.join(key_path), + env_key: env_key, + raise_if_missing_key: config.require_master_key + ) end def to_app #:nodoc: diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 290ec13878..6743ab2a54 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -16,44 +16,48 @@ module Rails :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, - :read_encrypted_secrets, :log_level + :read_encrypted_secrets, :log_level, :content_security_policy_report_only, + :require_master_key attr_reader :encoding, :api_only def initialize(*) super - self.encoding = Encoding::UTF_8 - @allow_concurrency = nil - @consider_all_requests_local = false - @filter_parameters = [] - @filter_redirect = [] - @helpers_paths = [] - @public_file_server = ActiveSupport::OrderedOptions.new - @public_file_server.enabled = true - @public_file_server.index_name = "index" - @force_ssl = false - @ssl_options = {} - @session_store = nil - @time_zone = "UTC" - @beginning_of_week = :monday - @log_level = :debug - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @railties_order = [:all] - @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] - @reload_classes_only_on_change = true - @file_watcher = ActiveSupport::FileUpdateChecker - @exceptions_app = nil - @autoflush_log = true - @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @eager_load = nil - @secret_token = nil - @secret_key_base = nil - @api_only = false - @debug_exception_response_format = nil - @x = Custom.new - @enable_dependency_loading = false - @read_encrypted_secrets = false + self.encoding = Encoding::UTF_8 + @allow_concurrency = nil + @consider_all_requests_local = false + @filter_parameters = [] + @filter_redirect = [] + @helpers_paths = [] + @public_file_server = ActiveSupport::OrderedOptions.new + @public_file_server.enabled = true + @public_file_server.index_name = "index" + @force_ssl = false + @ssl_options = {} + @session_store = nil + @time_zone = "UTC" + @beginning_of_week = :monday + @log_level = :debug + @generators = app_generators + @cache_store = [ :file_store, "#{root}/tmp/cache/" ] + @railties_order = [:all] + @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @reload_classes_only_on_change = true + @file_watcher = ActiveSupport::FileUpdateChecker + @exceptions_app = nil + @autoflush_log = true + @log_formatter = ActiveSupport::Logger::SimpleFormatter.new + @eager_load = nil + @secret_token = nil + @secret_key_base = nil + @api_only = false + @debug_exception_response_format = nil + @x = Custom.new + @enable_dependency_loading = false + @read_encrypted_secrets = false + @content_security_policy = nil + @content_security_policy_report_only = false + @require_master_key = false end def load_defaults(target_version) @@ -71,7 +75,6 @@ module Rails end self.ssl_options = { hsts: { subdomains: true } } - when "5.1" load_defaults "5.0" @@ -82,7 +85,6 @@ module Rails if respond_to?(:action_view) action_view.form_with_generates_remote_forms = true end - when "5.2" load_defaults "5.1" @@ -100,12 +102,19 @@ module Rails if respond_to?(:active_support) active_support.use_authenticated_message_encryption = true + active_support.use_sha1_digests = true end if respond_to?(:action_controller) action_controller.default_protect_from_forgery = true end + if respond_to?(:action_view) + action_view.form_with_generates_ids = true + end + when "6.0" + load_defaults "5.2" + else raise "Unknown version #{target_version.to_s.inspect}" end @@ -228,6 +237,10 @@ module Rails SourceAnnotationExtractor::Annotation end + def content_security_policy(&block) + @content_security_policy ||= ActionDispatch::ContentSecurityPolicy.new(&block) + end + class Custom #:nodoc: def initialize @configurations = Hash.new diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index ea2273c1f2..73c7defe7f 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -63,9 +63,14 @@ module Rails middleware.use ::ActionDispatch::Flash end + unless config.api_only + middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware + end + middleware.use ::Rack::Head middleware.use ::Rack::ConditionalGet middleware.use ::Rack::ETag, "no-cache" + middleware.use ::Rack::TempfileReaper end end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 3d938be951..c4b188aeee 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -58,7 +58,7 @@ module Rails end # This needs to happen before eager load so it happens - # in exactly the same point regardless of config.cache_classes + # in exactly the same point regardless of config.eager_load initializer :run_prepare_callbacks do |app| app.reloader.prepare! end diff --git a/railties/lib/rails/command/helpers/editor.rb b/railties/lib/rails/command/helpers/editor.rb new file mode 100644 index 0000000000..6191d97672 --- /dev/null +++ b/railties/lib/rails/command/helpers/editor.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "active_support/encrypted_file" + +module Rails + module Command + module Helpers + module Editor + private + def ensure_editor_available(command:) + if ENV["EDITOR"].to_s.empty? + say "No $EDITOR to open file in. Assign one like this:" + say "" + say %(EDITOR="mate --wait" #{command}) + say "" + say "For editors that fork and exit immediately, it's important to pass a wait flag," + say "otherwise the credentials will be saved immediately with no chance to edit." + + false + else + true + end + end + + def catch_editing_exceptions + yield + rescue Interrupt + say "Aborted changing file: nothing saved." + rescue ActiveSupport::EncryptedFile::MissingKeyError => error + say error.message + end + end + end + end +end diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb index 1ef7c1f343..fa54c0362a 100644 --- a/railties/lib/rails/commands/credentials/credentials_command.rb +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true require "active_support" +require "rails/command/helpers/editor" module Rails module Command class CredentialsCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + no_commands do def help say "Usage:\n #{self.class.banner}" @@ -16,43 +19,27 @@ module Rails def edit require_application_and_environment! - ensure_editor_available || (return) - ensure_master_key_has_been_added + 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 - change_credentials_in_system_editor + catch_editing_exceptions do + change_credentials_in_system_editor + end say "New credentials encrypted and saved." - rescue Interrupt - say "Aborted changing credentials: nothing saved." - rescue ActiveSupport::EncryptedFile::MissingKeyError => error - say error.message end def show require_application_and_environment! - say Rails.application.credentials.read.presence || - "No credentials have been added yet. Use bin/rails credentials:edit to change that." + + say Rails.application.credentials.read.presence || missing_credentials_message end private - def ensure_editor_available - if ENV["EDITOR"].to_s.empty? - say "No $EDITOR to open credentials in. Assign one like this:" - say "" - say %(EDITOR="mate --wait" bin/rails credentials:edit) - say "" - say "For editors that fork and exit immediately, it's important to pass a wait flag," - say "otherwise the credentials will be saved immediately with no chance to edit." - - false - else - true - end - end - def ensure_master_key_has_been_added master_key_generator.add_master_key_file + master_key_generator.ignore_master_key_file end def ensure_credentials_have_been_added @@ -79,6 +66,14 @@ module Rails Rails::Generators::CredentialsGenerator.new end + + def missing_credentials_message + if Rails.application.credentials.key.nil? + "Missing master key to decrypt credentials. See bin/rails credentials:help" + else + "No credentials have been added yet. Use bin/rails credentials:edit to change that." + end + end end end end diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb new file mode 100644 index 0000000000..3bc8f76ce4 --- /dev/null +++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "pathname" +require "active_support" +require "rails/command/helpers/editor" + +module Rails + module Command + class EncryptedCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + + class_option :key, aliases: "-k", type: :string, + default: "config/master.key", desc: "The Rails.root relative path to the encryption key" + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + end + end + + def edit(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + ensure_editor_available(command: "bin/rails encrypted:edit") || (return) + ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil? + ensure_encrypted_file_has_been_added(file_path, options[:key]) + + catch_editing_exceptions do + change_encrypted_file_in_system_editor(file_path, options[:key]) + end + + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?" + end + + def show(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path) + end + + private + 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_encrypted_file_has_been_added(file_path, key_path) + encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) + end + + 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 encryption_key_file_generator + require "rails/generators" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" + + Rails::Generators::EncryptionKeyFileGenerator.new + end + + def encrypted_file_generator + require "rails/generators" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" + + Rails::Generators::EncryptedFileGenerator.new + end + + def missing_encrypted_message(key:, key_path:, file_path:) + if key.nil? + "Missing '#{key_path}' to decrypt data. See bin/rails encrypted:help" + else + "File '#{file_path}' does not exist. Use bin/rails encrypted:edit #{file_path} to change that." + end + end + end + end +end diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb index c91139e33b..a36ccf314c 100644 --- a/railties/lib/rails/commands/secrets/secrets_command.rb +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -15,7 +15,7 @@ module Rails end def setup - generator.start + deprecate_in_favor_of_credentials_and_exit end def edit @@ -42,11 +42,10 @@ module Rails rescue Rails::Secrets::MissingKeyError => error say error.message rescue Errno::ENOENT => error - raise unless error.message =~ /secrets\.yml\.enc/ - - Rails::Secrets.read_template_for_editing do |tmp_path| - system("#{ENV["EDITOR"]} #{tmp_path}") - generator.skip_secrets_file { setup } + if error.message =~ /secrets\.yml\.enc/ + deprecate_in_favor_of_credentials_and_exit + else + raise end end @@ -55,11 +54,11 @@ module Rails end private - def generator - require "rails/generators" - require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + def deprecate_in_favor_of_credentials_and_exit + say "Encrypted secrets is deprecated in favor of credentials. Run:" + say "bin/rails credentials:help" - Rails::Generators::EncryptedSecretsGenerator + exit 1 end end end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 703ec59087..e546fe3e4b 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -27,7 +27,7 @@ module Rails app = super if app.is_a?(Class) ActiveSupport::Deprecation.warn(<<-MSG.squish) - Use `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. + Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. Please change `run #{app}` to `run Rails.application` in config.ru. MSG end diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 92b5e0392a..54bfbdd516 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -7,8 +7,8 @@ module Rails end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 5592e8d78e..6a7175c02b 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -218,6 +218,9 @@ module Rails rails.delete("app") rails.delete("plugin") rails.delete("encrypted_secrets") + rails.delete("encrypted_file") + rails.delete("encryption_key_file") + rails.delete("master_key") rails.delete("credentials") hidden_namespaces.each { |n| groups.delete(n.to_s) } @@ -274,8 +277,9 @@ module Rails else options = sorted_groups.flat_map(&:last) suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) + suggestions.map! { |s| "'#{s}'" } msg = "Could not find generator '#{namespace}'. ".dup - msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n" + msg << "Maybe you meant #{ suggestions[0...-1].join(', ')} or #{suggestions[-1]}\n" msg << "Run `rails generate --help` for more options." puts msg end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index bdeddff645..863a914912 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -84,6 +84,9 @@ module Rails class_option :skip_system_test, type: :boolean, default: false, desc: "Skip system test files" + class_option :skip_bootsnap, type: :boolean, default: false, + desc: "Skip bootsnap gem" + class_option :dev, type: :boolean, default: false, desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" @@ -192,7 +195,7 @@ module Rails def webserver_gemfile_entry # :doc: return [] if options[:skip_puma] comment = "Use Puma as the app server" - GemfileEntry.new("puma", "~> 3.7", comment) + GemfileEntry.new("puma", "~> 3.11", comment) end def include_all_railties? # :doc: @@ -266,17 +269,14 @@ module Rails end def rails_gemfile_entry - dev_edge_common = [ - GemfileEntry.github("arel", "rails/arel"), - ] if options.dev? [ GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH) - ] + dev_edge_common + ] elsif options.edge? [ GemfileEntry.github("rails", "rails/rails") - ] + dev_edge_common + ] else [GemfileEntry.version("rails", rails_version_specifier, @@ -300,8 +300,8 @@ 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.3.18", "< 0.5"]] - when "postgresql" then ["pg", ["~> 0.18"]] + 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] when "sqlserver" then ["activerecord-sqlserver-adapter", nil] @@ -315,11 +315,13 @@ module Rails def convert_database_option_for_jruby if defined?(JRUBY_VERSION) - case options[:database] - when "postgresql" then options[:database].replace "jdbcpostgresql" - when "mysql" then options[:database].replace "jdbcmysql" - when "sqlite3" then options[:database].replace "jdbcsqlite3" + opt = options.dup + case opt[:database] + when "postgresql" then opt[:database] = "jdbcpostgresql" + when "mysql" then opt[:database] = "jdbcmysql" + when "sqlite3" then opt[:database] = "jdbcsqlite3" end + self.options = opt.freeze end end @@ -438,6 +440,10 @@ module Rails !options[:skip_listen] && os_supports_listen_out_of_the_box? end + def depend_on_bootsnap? + !options[:skip_bootsnap] && !options[:dev] + end + def os_supports_listen_out_of_the_box? RbConfig::CONFIG["host_os"] =~ /darwin|linux/ end @@ -459,16 +465,6 @@ module Rails end end - def run_active_storage - unless skip_active_storage? - if bundle_install? - rails_command "active_storage:install", capture: options[:quiet] - else - log("Active Storage installation was skipped. Please run `bin/rails active_storage:install` to install Active Storage files.") - end - end - end - def empty_directory_with_keep_file(destination, config = {}) empty_directory(destination, config) keep_file(destination) diff --git a/railties/lib/rails/generators/erb/controller/templates/view.html.erb b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt index cd54d13d83..cd54d13d83 100644 --- a/railties/lib/rails/generators/erb/controller/templates/view.html.erb +++ b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt index b5045671b3..b5045671b3 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt index 342285df19..342285df19 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt index 0eb9d82bbb..518cb1121e 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt @@ -15,15 +15,15 @@ <div class="field"> <% if attribute.password_digest? -%> <%%= form.label :password %> - <%%= form.password_field :password, id: :<%= field_id(:password) %> %> + <%%= form.password_field :password %> </div> <div class="field"> <%%= form.label :password_confirmation %> - <%%= form.password_field :password_confirmation, id: :<%= field_id(:password_confirmation) %> %> + <%%= form.password_field :password_confirmation %> <% else -%> <%%= form.label :<%= attribute.column_name %> %> - <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, id: :<%= field_id(attribute.column_name) %> %> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> <% end -%> </div> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt index 81329473d9..81329473d9 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt index e1ede7c713..e1ede7c713 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt index 9b2b2f4875..9b2b2f4875 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt index 5e634153be..5e634153be 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 2728459968..f7fd30a5fb 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -75,7 +75,7 @@ module Rails when :date then :date_select when :text then :text_area when :boolean then :check_box - else + else :text_field end end @@ -91,7 +91,7 @@ module Rails when :text then "MyText" when :boolean then false when :references, :belongs_to then nil - else + else "" end end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 99165168fd..98fcc95964 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -114,10 +114,6 @@ module Rails "new_#{singular_route_name}_url" end - def field_id(attribute_name) - [singular_table_name, attribute_name].join("_") - end - def singular_table_name # :doc: @singular_table_name ||= (pluralize_table_names? ? table_name.singularize : table_name) end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 1c32fad3ea..fd9da7803f 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -128,6 +128,7 @@ module Rails active_storage_config_exist = File.exist?("config/storage.yml") rack_cors_config_exist = File.exist?("config/initializers/cors.rb") assets_config_exist = File.exist?("config/initializers/assets.rb") + csp_config_exist = File.exist?("config/initializers/content_security_policy.rb") config @@ -155,27 +156,27 @@ module Rails unless assets_config_exist remove_file "config/initializers/assets.rb" end + + unless csp_config_exist + remove_file "config/initializers/content_security_policy.rb" + end end end def master_key - return if options[:pretend] + return if options[:pretend] || options[:dummy_app] require "rails/generators/rails/master_key/master_key_generator" - - after_bundle do - Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet]).add_master_key_file - end + master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet]) + master_key_generator.add_master_key_file_silently + master_key_generator.ignore_master_key_file_silently end def credentials - return if options[:pretend] + return if options[:pretend] || options[:dummy_app] require "rails/generators/rails/credentials/credentials_generator" - - after_bundle do - Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently - end + Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently end def database_yml @@ -347,6 +348,14 @@ module Rails build(:public_directory) end + def create_tmp_files + build(:tmp) + end + + def create_vendor_files + build(:vendor) + end + def create_test_files build(:test) unless options[:skip_test] end @@ -359,14 +368,6 @@ module Rails build(:storage) unless skip_active_storage? end - def create_tmp_files - build(:tmp) - end - - def create_vendor_files - build(:vendor) - end - def delete_app_assets_if_api_option if options[:api] remove_dir "app/assets" @@ -436,6 +437,7 @@ module Rails def delete_non_api_initializers_if_api_option if options[:api] remove_file "config/initializers/cookies_serializer.rb" + remove_file "config/initializers/content_security_policy.rb" end end @@ -461,7 +463,6 @@ module Rails public_task :apply_rails_template, :run_bundle public_task :run_webpack, :generate_spring_binstubs - public_task :run_active_storage def run_after_bundle_callbacks @after_bundle_callbacks.each(&:call) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 61026f5182..23bb89f4ce 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -29,9 +29,11 @@ ruby <%= "'#{RUBY_VERSION}'" -%> # Use Capistrano for deployment # gem 'capistrano-rails', group: :development +<% if depend_on_bootsnap? -%> # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false +<%- end -%> <%- if options.api? -%> # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible # gem 'rack-cors' @@ -41,13 +43,6 @@ gem 'bootsnap', '>= 1.1.0', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] - <%- if depends_on_system_test? -%> - # Adds support for Capybara system testing and selenium driver - gem 'capybara', '~> 2.15' - gem 'selenium-webdriver' - # Easy installation and use of chromedriver to run system tests with Chrome - gem 'chromedriver-helper' - <%- end -%> end group :development do @@ -70,6 +65,16 @@ group :development do <% end -%> <% end -%> end + +<%- if depends_on_system_test? -%> +group :test do + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '~> 2.15' + gem 'selenium-webdriver' + # Easy installation and use of chromedriver to run system tests with Chrome + gem 'chromedriver-helper' +end +<%- end -%> <% end -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/railties/lib/rails/generators/rails/app/templates/README.md b/railties/lib/rails/generators/rails/app/templates/README.md.tt index 7db80e4ca1..7db80e4ca1 100644 --- a/railties/lib/rails/generators/rails/app/templates/README.md +++ b/railties/lib/rails/generators/rails/app/templates/README.md.tt diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt index e85f913914..e85f913914 100644 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js.tt index 739aa5f022..739aa5f022 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt index d05ea0f511..d05ea0f511 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt index d672697283..d672697283 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt index 0ff5442f47..0ff5442f47 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt index de6be7945c..de6be7945c 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt index a009ace51c..a009ace51c 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt index 286b2239d1..286b2239d1 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt index 10a4cba84d..10a4cba84d 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt index a84f0afe47..a84f0afe47 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/bundle +++ b/railties/lib/rails/generators/rails/app/templates/bin/bundle.tt diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt index 513a2e0183..513a2e0183 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/rails +++ b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rake b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt index d14fc8395b..d14fc8395b 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/rake +++ b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt index b4e4d95286..90ddcc520e 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/yarn +++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt @@ -1,7 +1,7 @@ APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do begin - exec "yarnpkg #{ARGV.join(' ')}" + exec "yarnpkg", *ARGV rescue Errno::ENOENT $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru.tt index f7ba0b527b..f7ba0b527b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru +++ b/railties/lib/rails/generators/rails/app/templates/config.ru.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt index 9e03e86771..d1a09f9c3c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt @@ -27,8 +27,9 @@ module <%= app_const_base %> config.load_defaults <%= Rails::VERSION::STRING.to_f %> # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. <%- if options.api? -%> # Only loads a smaller set of middleware suitable for API only apps. diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt index b9e460cef3..42d46b8175 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt @@ -1,4 +1,6 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. +<% if depend_on_bootsnap? -%> require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt index 8e53156c71..8e53156c71 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt index 917b52e535..917b52e535 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt index d40117a27f..d40117a27f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt index 563be77710..563be77710 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt index 2a67bdca25..2a67bdca25 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt index 70df04079d..70df04079d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt index 371415e6a8..371415e6a8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt index 04afaa0596..04afaa0596 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt index 6da0601b24..6da0601b24 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt index 145cfb7f74..145cfb7f74 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt index 9510568124..9510568124 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt index 049de65f22..049de65f22 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt index 426333bb46..426333bb46 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index b383228dc0..a87649b50f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -14,7 +14,7 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp/caching-dev.txt').exist? + if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store @@ -46,6 +46,9 @@ Rails.application.configure do # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + <%- end -%> <%- unless options.skip_sprockets? -%> # Debug mode disables concatenation and preprocessing of assets. 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 4c0f36db98..926326b5bb 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 @@ -34,8 +34,6 @@ Rails.application.configure do # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - <%- end -%> # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt index 89d2efab2b..89d2efab2b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt index 59385cdf37..59385cdf37 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt 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 new file mode 100644 index 0000000000..edde7f42b8 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https, :unsafe_inline + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt index 5a6a32d371..5a6a32d371 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt index 3b1c1b5ed1..3b1c1b5ed1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt index 4a994e1e7b..4a994e1e7b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt index ac033bf9dc..ac033bf9dc 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt index dc1899682b..dc1899682b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt index 25dcddb27a..b4ef455802 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt @@ -10,7 +10,7 @@ # This is needed for recyclable cache keys. # Rails.application.config.active_record.cache_versioning = true -# Use AES 256 GCM authenticated encryption for encrypted cookies. +# Use AES-256-GCM authenticated encryption for encrypted cookies. # Existing cookies will be converted on read then written with the new scheme. # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true @@ -25,3 +25,6 @@ # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and # 'f' after migrating old data. # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true + +# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. +# Rails.application.config.active_support.use_sha1_digests = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt index 1e19380dcb..a5eccf816b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt @@ -26,31 +26,9 @@ environment ENV.fetch("RAILS_ENV") { "development" } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. If you use this option -# you need to make sure to reconnect any threads in the `on_worker_boot` -# block. +# process behavior so workers use less memory. # # preload_app! -# If you are preloading your application and using Active Record, it's -# recommended that you close any connections to the database before workers -# are forked to prevent connection leakage. -# -# before_fork do -# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) -# end - -# The code in the `on_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted, this block will be run. If you are using the `preload_app!` -# option, you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, as Ruby -# cannot share connections between processes. -# -# on_worker_boot do -# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) -# end -# - # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt index 787824f888..787824f888 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/spring.rb b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt index 9fa7863f99..9fa7863f99 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/spring.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/storage.yml b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt index 9bada4b66d..1c0cde0b09 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/storage.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt @@ -18,7 +18,7 @@ local: # google: # service: GCS # project: your_project -# keyfile: <%%= Rails.root.join("path/to/gcs.keyfile") %> +# credentials: <%%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore.tt index 2cd8335aba..2cd8335aba 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt diff --git a/railties/lib/rails/generators/rails/app/templates/package.json b/railties/lib/rails/generators/rails/app/templates/package.json.tt index 46db57dcbe..46db57dcbe 100644 --- a/railties/lib/rails/generators/rails/app/templates/package.json +++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt index c444f33b0f..c444f33b0f 100644 --- a/railties/lib/rails/generators/rails/app/templates/ruby-version +++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt diff --git a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt index d19212abd5..d19212abd5 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt index 6ad1f11781..52d68cc77c 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt @@ -1,3 +1,4 @@ +ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' require 'rails/test_help' diff --git a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css index 7594abf268..afad32db02 100644 --- a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css +++ b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css @@ -1,4 +1,4 @@ -/* +/* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ diff --git a/railties/lib/rails/generators/rails/controller/templates/controller.rb b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt index 633e0b3177..633e0b3177 100644 --- a/railties/lib/rails/generators/rails/controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt diff --git a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb index 52cb4bd8bf..0fb4d5fbd1 100644 --- a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb +++ b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb @@ -2,19 +2,17 @@ require "rails/generators/base" require "rails/generators/rails/master_key/master_key_generator" +require "active_support/core_ext/string/strip" require "active_support/encrypted_configuration" module Rails module Generators - class CredentialsGenerator < Base - CONFIG_PATH = "config/credentials.yml.enc" - KEY_PATH = "config/master.key" - + class CredentialsGenerator < Base # :nodoc: def add_credentials_file - unless File.exist?(CONFIG_PATH) + unless credentials.content_path.exist? template = credentials_template - say "Adding #{CONFIG_PATH} to store encrypted credentials." + say "Adding #{credentials.content_path} to store encrypted credentials." say "" say "The following content has been encrypted with the Rails master key:" say "" @@ -29,17 +27,30 @@ module Rails end def add_credentials_file_silently(template = nil) - unless File.exist?(CONFIG_PATH) - setup = { config_path: CONFIG_PATH, key_path: KEY_PATH, env_key: "RAILS_MASTER_KEY" } - ActiveSupport::EncryptedConfiguration.new(setup).write(credentials_template) + unless credentials.content_path.exist? + credentials.write(credentials_template) end end private + def credentials + ActiveSupport::EncryptedConfiguration.new( + config_path: "config/credentials.yml.enc", + key_path: "config/master.key", + env_key: "RAILS_MASTER_KEY", + raise_if_missing_key: true + ) + end + def credentials_template - "# aws:\n# access_key_id: 123\n# secret_access_key: 345\n\n" + - "# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.\n" + - "secret_key_base: #{SecureRandom.hex(64)}" + <<-YAML.strip_heredoc + # aws: + # access_key_id: 123 + # secret_access_key: 345 + + # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. + secret_key_base: #{SecureRandom.hex(64)} + YAML end end end diff --git a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb new file mode 100644 index 0000000000..ef398f52a1 --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails/generators/base" +require "active_support/core_ext/string/strip" +require "active_support/encrypted_file" + +module Rails + module Generators + class EncryptedFileGenerator < Base # :nodoc: + def add_encrypted_file_silently(file_path, key_path, template = encrypted_file_template) + unless File.exist?(file_path) + setup = { content_path: file_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true } + ActiveSupport::EncryptedFile.new(setup).write(template) + end + end + + private + def encrypted_file_template + <<-YAML.strip_heredoc + # aws: + # access_key_id: 123 + # secret_access_key: 345 + + YAML + end + end + end +end diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb deleted file mode 100644 index 1aa7a2622a..0000000000 --- a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators/base" -require "rails/secrets" - -module Rails - module Generators - class EncryptedSecretsGenerator < Base - def add_secrets_key_file - unless File.exist?("config/secrets.yml.key") || File.exist?("config/secrets.yml.enc") - key = Rails::Secrets.generate_key - - say "Adding config/secrets.yml.key to store the encryption key: #{key}" - say "" - say "Save this in a password manager your team can access." - say "" - say "If you lose the key, no one, including you, can access any encrypted secrets." - - say "" - create_file "config/secrets.yml.key", key - say "" - end - end - - def ignore_key_file - if File.exist?(".gitignore") - unless File.read(".gitignore").include?(key_ignore) - say "Ignoring config/secrets.yml.key so it won't end up in Git history:" - say "" - append_to_file ".gitignore", key_ignore - say "" - end - else - say "IMPORTANT: Don't commit config/secrets.yml.key. Add this to your ignore file:" - say key_ignore, :on_green - say "" - end - end - - def add_encrypted_secrets_file - unless (defined?(@@skip_secrets_file) && @@skip_secrets_file) || File.exist?("config/secrets.yml.enc") - say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted." - say "" - say "For now the file contains this but it's been encrypted with the generated key:" - say "" - say Secrets.template, :on_green - say "" - - Secrets.write(Secrets.template) - - say "You can edit encrypted secrets with `bin/rails secrets:edit`." - say "" - end - - say "Add this to your config/environments/production.rb:" - say "config.read_encrypted_secrets = true" - end - - def self.skip_secrets_file - @@skip_secrets_file = true - yield - ensure - @@skip_secrets_file = false - end - - private - def key_ignore - [ "", "# Ignore encrypted secrets key file.", "config/secrets.yml.key", "" ].join("\n") - end - end - end -end diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb new file mode 100644 index 0000000000..90068c678d --- /dev/null +++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "pathname" +require "rails/generators/base" +require "active_support/encrypted_file" + +module Rails + module Generators + class EncryptionKeyFileGenerator < Base # :nodoc: + def add_key_file(key_path) + key_path = Pathname.new(key_path) + + unless key_path.exist? + key = ActiveSupport::EncryptedFile.generate_key + + log "Adding #{key_path} to store the encryption key: #{key}" + log "" + log "Save this in a password manager your team can access." + log "" + log "If you lose the key, no one, including you, can access anything encrypted with it." + + log "" + add_key_file_silently(key_path, key) + log "" + end + end + + def add_key_file_silently(key_path, key = nil) + create_file key_path, key || ActiveSupport::EncryptedFile.generate_key + end + + def ignore_key_file(key_path, ignore: key_ignore(key_path)) + if File.exist?(".gitignore") + unless File.read(".gitignore").include?(ignore) + log "Ignoring #{key_path} so it won't end up in Git history:" + log "" + append_to_file ".gitignore", ignore + log "" + end + else + log "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:" + log ignore, :on_green + log "" + end + end + + def ignore_key_file_silently(key_path, ignore: key_ignore(key_path)) + append_to_file ".gitignore", ignore if File.exist?(".gitignore") + end + + private + def key_ignore(key_path) + [ "", "/#{key_path}", "" ].join("\n") + end + end + end +end diff --git a/railties/lib/rails/generators/rails/helper/templates/helper.rb b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt index b4173151b4..b4173151b4 100644 --- a/railties/lib/rails/generators/rails/helper/templates/helper.rb +++ b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb index 23dbed20ac..82968985dc 100644 --- a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb +++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require "rails/generators/base" require "pathname" +require "rails/generators/base" +require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" require "active_support/encrypted_file" module Rails module Generators - class MasterKeyGenerator < Base + class MasterKeyGenerator < Base # :nodoc: MASTER_KEY_PATH = Pathname.new("config/master.key") def add_master_key_file @@ -20,27 +21,26 @@ module Rails log "If you lose the key, no one, including you, can access anything encrypted with it." log "" - create_file MASTER_KEY_PATH, key + add_master_key_file_silently(key) log "" - - ignore_master_key_file end end + def add_master_key_file_silently(key = nil) + key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key) + end + + def ignore_master_key_file + key_file_generator.ignore_key_file(MASTER_KEY_PATH, ignore: key_ignore) + end + + def ignore_master_key_file_silently + key_file_generator.ignore_key_file_silently(MASTER_KEY_PATH, ignore: key_ignore) + end + private - def ignore_master_key_file - if File.exist?(".gitignore") - unless File.read(".gitignore").include?(key_ignore) - log "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:" - log "" - append_to_file ".gitignore", key_ignore - log "" - end - else - log "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:" - log key_ignore, :on_green - log "" - end + def key_file_generator + EncryptionKeyFileGenerator.new([], options) end def key_ignore diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index bc2dcf008e..a83c911806 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -98,6 +98,7 @@ task default: :test opts[:skip_listen] = true opts[:skip_git] = true opts[:skip_turbolinks] = true + opts[:dummy_app] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -166,7 +167,7 @@ task default: :test gemfile_in_app_path = File.join(rails_app_path, "Gemfile") if File.exist? gemfile_in_app_path - entry = "gem '#{name}', path: '#{relative_path}'" + entry = "\ngem '#{name}', path: '#{relative_path}'" append_file gemfile_in_app_path, entry end end @@ -262,6 +263,10 @@ task default: :test public_task :apply_rails_template def run_after_bundle_callbacks + unless @after_bundle_callbacks.empty? + ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ") + end + @after_bundle_callbacks.each do |callback| callback.call end diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt index 9a8c4bf098..9a8c4bf098 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt index 290259b4db..290259b4db 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt index ff2fb3ba4e..ff2fb3ba4e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE +++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.md b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt index 1632409bea..1632409bea 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/README.md +++ b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt index f3efe21cf1..f3efe21cf1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt index 154452bfe5..154452bfe5 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt index 7a68da5c4b..7a68da5c4b 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt index 3285055eb7..3285055eb7 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt index 8938770fc4..8938770fc4 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt index 7bdf4ee5fb..7bdf4ee5fb 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt index b08f4ef9ae..b08f4ef9ae 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt index 88a2c4120f..88a2c4120f 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt index 06ffe2f1ed..06ffe2f1ed 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt index c9aef85d40..c9aef85d40 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt index 03937cf8ff..03937cf8ff 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt index 2f23844f5e..2f23844f5e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt index f3d80c87f5..f3d80c87f5 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt index 694510edc0..694510edc0 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt index 1ee05d7871..1ee05d7871 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt index d19212abd5..d19212abd5 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt index 29e59d8407..29e59d8407 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt index 7fa9973931..755d19ef5d 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt @@ -1,3 +1,6 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + require_relative "<%= File.join('..', options[:dummy_path], 'config/environment') -%>" <% unless options[:skip_active_record] -%> ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)] diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt index 400afec6dc..400afec6dc 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt index 05f1c2b2d3..05f1c2b2d3 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt diff --git a/railties/lib/rails/generators/rails/task/templates/task.rb b/railties/lib/rails/generators/rails/task/templates/task.rb.tt index 1e3ed5f158..1e3ed5f158 100644 --- a/railties/lib/rails/generators/rails/task/templates/task.rb +++ b/railties/lib/rails/generators/rails/task/templates/task.rb.tt diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt index ff41fef9e9..ff41fef9e9 100644 --- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt index a7f1fc4fba..a7f1fc4fba 100644 --- a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb +++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt index 118e0f1271..118e0f1271 100644 --- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb +++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb index 9225af4e0c..a99ce914c0 100644 --- a/railties/lib/rails/generators/test_unit/job/job_generator.rb +++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb @@ -8,7 +8,7 @@ module TestUnit # :nodoc: check_class_collision suffix: "JobTest" def create_test_file - template "unit_test.rb.erb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb") + template "unit_test.rb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb") end end end diff --git a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt index f5351d0ec6..f5351d0ec6 100644 --- a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb +++ b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt index a2f2d30de5..a2f2d30de5 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt index b063cbc47b..b063cbc47b 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt index 0681780c97..0681780c97 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt diff --git a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt index c9bc7d5b90..c9bc7d5b90 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb +++ b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index b6c13b41ae..e2e8b18eab 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -23,7 +23,7 @@ module TestUnit # :nodoc: template template_file, File.join("test/controllers", controller_class_path, "#{controller_file_name}_controller_test.rb") - unless options.api? || options[:system_tests].nil? + if !options.api? && options[:system_tests] template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb") end end diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt index f21861d8e6..f21861d8e6 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt index 195d60be20..195d60be20 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt index f83f5a5c62..f83f5a5c62 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt index d19212abd5..d19212abd5 100644 --- a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb +++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt diff --git a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt index b5ce2ba5c8..b5ce2ba5c8 100644 --- a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb +++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index d8f361f524..d5c9973c6b 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -99,7 +99,7 @@ module Rails end property "Database schema version" do - ActiveRecord::Migrator.current_version rescue nil + ActiveRecord::Base.connection.migration_context.current_version rescue nil end end end diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 66636e5d6b..0b0e802358 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -6,9 +6,9 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH before_action :require_local!, unless: :show_previews? - before_action :find_preview, only: :preview + before_action :find_preview, :set_locale, only: :preview - helper_method :part_query + helper_method :part_query, :locale_query def index @previews = ActionMailer::Preview.all @@ -84,4 +84,12 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: def part_query(mime_type) request.query_parameters.merge(part: mime_type).to_query end + + def locale_query(locale) + request.query_parameters.merge(locale: locale).to_query + end + + def set_locale + I18n.locale = params[:locale] || I18n.default_locale + end end diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index 48853129bc..70274b948c 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -55,7 +55,7 @@ module Rails ActiveSupport.on_load(:before_configuration, yield: true, &block) end - # Third configurable block to run. Does not run if +config.cache_classes+ + # Third configurable block to run. Does not run if +config.eager_load+ # set to false. def before_eager_load(&block) ActiveSupport.on_load(:before_eager_load, yield: true, &block) diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb index aea72b2d01..30e3478c9b 100644 --- a/railties/lib/rails/secrets.rb +++ b/railties/lib/rails/secrets.rb @@ -32,23 +32,10 @@ module Rails end end - def generate_key - SecureRandom.hex(OpenSSL::Cipher.new(@cipher).key_len) - end - def key ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key end - def template - <<-end_of_template.strip_heredoc - # See `secrets.yml` for tips on generating suitable keys. - # production: - # external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289 - - end_of_template - end - def encrypt(data) encryptor.encrypt_and_sign(data) end @@ -70,10 +57,6 @@ module Rails writing(read, &block) end - def read_template_for_editing(&block) - writing(template, &block) - end - private def handle_missing_key raise MissingKeyError diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake index c24d1ad532..8d77904210 100644 --- a/railties/lib/rails/tasks/engine.rake +++ b/railties/lib/rails/tasks/engine.rake @@ -53,7 +53,7 @@ namespace :db do desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." app_task "rollback" - desc "Create a db/schema.rb file that can be portably used against any DB supported by Active Record" + desc "Create a db/schema.rb file that can be portably used against any database supported by Active Record" app_task "schema:dump" desc "Load a schema.rb file into the database" @@ -62,7 +62,7 @@ namespace :db do desc "Load the seed data from db/seeds.rb" app_task "seed" - desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)" + desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)" app_task "setup" desc "Dump the database structure to an SQL file" @@ -70,6 +70,9 @@ namespace :db do desc "Retrieves the current schema version number" app_task "version" + + # desc 'Load the test schema' + app_task "test:prepare" end def find_engine_path(path) diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index 89c1129f90..2a41c29602 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -95,11 +95,25 @@ </dd> <% end %> + <dt>Format:</dt> <% if @email.multipart? %> <dd> - <select onchange="formatChanged(this);"> - <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="?<%= part_query('text/html') %>">View as HTML email</option> - <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="?<%= part_query('text/plain') %>">View as plain-text email</option> + <select id="part" onchange="refreshBody();"> + <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option> + <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option> + </select> + </dd> + <% else %> + <dd id="mime_type" data-mime-type="<%= part_query(@email.mime_type) %>"><%= @email.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd> + <% end %> + + <% if I18n.available_locales.count > 1 %> + <dt>Locale:</dt> + <dd> + <select id="locale" onchange="refreshBody();"> + <% I18n.available_locales.each do |locale| %> + <option <%= I18n.locale == locale ? 'selected' : '' %> value="<%= locale_query(locale) %>"><%= locale %></option> + <% end %> </select> </dd> <% end %> @@ -116,15 +130,27 @@ <% end %> <script> - function formatChanged(form) { - var part_name = form.options[form.selectedIndex].value - var iframe =document.getElementsByName('messageBody')[0]; - iframe.contentWindow.location.replace(part_name); + function refreshBody() { + var part_select = document.querySelector('select#part'); + var locale_select = document.querySelector('select#locale'); + var iframe = document.getElementsByName('messageBody')[0]; + var part_param = part_select ? + part_select.options[part_select.selectedIndex].value : + document.querySelector('#mime_type').dataset.mimeType; + var locale_param = locale_select ? locale_select.options[locale_select.selectedIndex].value : null; + var fresh_location; + if (locale_param) { + fresh_location = '?' + part_param + '&' + locale_param; + } else { + fresh_location = '?' + part_param; + } + iframe.contentWindow.location = fresh_location; if (history.replaceState) { - var url = location.pathname.replace(/\.(txt|html)$/, ''); - var format = /html/.test(part_name) ? '.html' : '.txt'; - window.history.replaceState({}, '', url + format); + var url = location.pathname.replace(/\.(txt|html)$/, ''); + var format = /html/.test(part_param) ? '.html' : '.txt'; + var state_to_replace = locale_param ? (url + format + '?' + locale_param) : (url + format); + window.history.replaceState({}, '', state_to_replace); } } </script> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 732c5c1e1f..76c28ac85e 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -29,10 +29,6 @@ if defined?(ActiveRecord::Base) end ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path - - def create_fixtures(*fixture_set_names, &block) - FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, {}, &block) - end end # :enddoc: diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index 7d3164f1eb..28b93cee5a 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -64,11 +64,17 @@ module Rails end def format_line(result) - "%s#%s = %.2f s = %s" % [result.class, result.name, result.time, result.result_code] + klass = result.respond_to?(:klass) ? result.klass : result.class + "%s#%s = %.2f s = %s" % [klass, result.name, result.time, result.result_code] end def format_rerun_snippet(result) - location, line = result.method(result.name).source_location + location, line = if result.respond_to?(:source_location) + result.source_location + else + result.method(result.name).source_location + end + "#{executable} #{relative_path_for(location)}:#{line}" end diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb index 5c2f6451e2..de5744c662 100644 --- a/railties/lib/rails/test_unit/runner.rb +++ b/railties/lib/rails/test_unit/runner.rb @@ -13,7 +13,7 @@ module Rails class << self def attach_before_load_options(opts) opts.on("--warnings", "-w", "Run with Ruby warnings enabled") {} - opts.on("--environment", "-e", "Run tests in the ENV environment") {} + opts.on("-e", "--environment ENV", "Run tests in the ENV environment") {} end def parse_options(argv) diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index 05b17b4a7a..608bc2fbe3 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -33,7 +33,7 @@ module ApplicationTests assert_nil x.i_do_not_exist.zomg # test that custom configuration responds to all messages - assert_equal true, x.respond_to?(:i_do_not_exist) + assert_respond_to x, :i_do_not_exist assert_kind_of Method, x.method(:i_do_not_exist) assert_kind_of ActiveSupport::OrderedOptions, x.i_do_not_exist end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index bb8cc0876c..236d73d5fd 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -94,7 +94,7 @@ module ApplicationTests with_rails_env "development" do app "development" - assert Rails.application.config.log_tags.blank? + assert_predicate Rails.application.config.log_tags, :blank? end end @@ -707,6 +707,14 @@ module ApplicationTests assert_match(/Missing.*RAILS_MASTER_KEY/, error) end + test "credentials does not raise error when require_master_key is false and master key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = false" + app "development" + + assert_not app.credentials.secret_key_base + end + test "protect from forgery is the default in a new app" do make_basic_app @@ -757,6 +765,68 @@ module ApplicationTests assert_match(/label/, last_response.body) end + test "form_with can be configured with form_with_generates_ids" do + app_file "config/initializers/form_builder.rb", <<-RUBY + Rails.configuration.action_view.form_with_generates_ids = false + RUBY + + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + + assert_no_match(/id=('|")post_name('|")/, last_response.body) + end + + test "form_with outputs ids by default" do + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + + assert_match(/id=('|")post_name('|")/, last_response.body) + end + test "form_with can be configured with form_with_generates_remote_forms" do app_file "config/initializers/form_builder.rb", <<-RUBY Rails.configuration.action_view.form_with_generates_remote_forms = false @@ -1255,7 +1325,7 @@ module ApplicationTests assert_equal 200, last_response.status end - test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do + test "config.action_controller.action_on_unpermitted_parameters is :log by default in development" do app "development" force_lazy_load_hooks { ActionController::Base } @@ -1264,7 +1334,7 @@ module ApplicationTests assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end - test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do + test "config.action_controller.action_on_unpermitted_parameters is :log by default in test" do app "test" force_lazy_load_hooks { ActionController::Base } @@ -1273,7 +1343,7 @@ module ApplicationTests assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end - test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do + test "config.action_controller.action_on_unpermitted_parameters is false by default in production" do app "production" force_lazy_load_hooks { ActionController::Base } @@ -1349,11 +1419,11 @@ module ApplicationTests test "Rails.application#env_config exists and include some existing parameters" do make_basic_app - assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters - assert_equal app.env_config["action_dispatch.show_exceptions"], app.config.action_dispatch.show_exceptions - assert_equal app.env_config["action_dispatch.logger"], Rails.logger - assert_equal app.env_config["action_dispatch.backtrace_cleaner"], Rails.backtrace_cleaner - assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator + assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters + assert_equal app.env_config["action_dispatch.show_exceptions"], app.config.action_dispatch.show_exceptions + assert_equal app.env_config["action_dispatch.logger"], Rails.logger + assert_equal app.env_config["action_dispatch.backtrace_cleaner"], Rails.backtrace_cleaner + assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator end test "config.colorize_logging default is true" do @@ -1408,7 +1478,7 @@ module ApplicationTests test "respond_to? accepts include_private" do make_basic_app - assert_not Rails.configuration.respond_to?(:method_missing) + assert_not_respond_to Rails.configuration, :method_missing assert Rails.configuration.respond_to?(:method_missing, true) end @@ -1420,12 +1490,18 @@ module ApplicationTests assert_not ActiveRecord::Base.dump_schema_after_migration end - test "config.active_record.dump_schema_after_migration is true by default on development" do + test "config.active_record.dump_schema_after_migration is true by default in development" do app "development" assert ActiveRecord::Base.dump_schema_after_migration end + test "config.active_record.verbose_query_logs is false by default in development" do + app "development" + + assert_not ActiveRecord::Base.verbose_query_logs + end + test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do make_basic_app do |application| application.config.annotations.register_extensions("coffee") do |tag| @@ -1663,9 +1739,7 @@ module ApplicationTests test "default SQLite3Adapter.represent_boolean_as_integer for 5.1 is false" do remove_from_config '.*config\.load_defaults.*\n' - add_to_top_of_config <<-RUBY - config.load_defaults 5.1 - RUBY + app_file "app/models/post.rb", <<-RUBY class Post < ActiveRecord::Base end @@ -1757,7 +1831,7 @@ module ApplicationTests test "api_only is false by default" do app "development" - refute Rails.application.config.api_only + assert_not Rails.application.config.api_only end test "api_only generator config is set when api_only is set" do @@ -1814,6 +1888,58 @@ module ApplicationTests assert_equal "https://example.org/", last_response.location end + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do + app "development" + + assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption + end + + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is false by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal false, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption + end + + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_5_2.rb", <<-RUBY + Rails.application.config.active_support.use_authenticated_message_encryption = true + RUBY + + app "development" + + assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption + end + + test "ActiveSupport::Digest.hash_digest_class is Digest::SHA1 by default for new apps" do + app "development" + + assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class + end + + test "ActiveSupport::Digest.hash_digest_class is Digest::MD5 by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal Digest::MD5, ActiveSupport::Digest.hash_digest_class + end + + test "ActiveSupport::Digest.hash_digest_class can be configured via config.active_support.use_sha1_digests" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_5_2.rb", <<-RUBY + Rails.application.config.active_support.use_sha1_digests = true + RUBY + + app "development" + + assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class + 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 13164f49c2..4a14042cd3 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -73,7 +73,7 @@ class ConsoleTest < ActiveSupport::TestCase MODEL load_environment - assert User.new.respond_to?(:name) + assert_respond_to User.new, :name app_file "app/models/user.rb", <<-MODEL class User @@ -81,9 +81,9 @@ class ConsoleTest < ActiveSupport::TestCase end MODEL - assert !User.new.respond_to?(:age) + assert_not_respond_to User.new, :age irb_context.reload!(false) - assert User.new.respond_to?(:age) + assert_respond_to User.new, :age end def test_access_to_helpers diff --git a/railties/test/application/content_security_policy_test.rb b/railties/test/application/content_security_policy_test.rb new file mode 100644 index 0000000000..97f2957c33 --- /dev/null +++ b/railties/test/application/content_security_policy_test.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" + +module ApplicationTests + class ContentSecurityPolicyTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + end + + def teardown + teardown_app + end + + test "default content security policy is empty" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_equal ";", last_response.headers["Content-Security-Policy"] + end + + test "global content security policy in an initializer" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:;" + end + + test "global report only content security policy in an initializer" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + + Rails.application.config.content_security_policy_report_only = true + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:;", report_only: true + end + + test "override content security policy in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + content_security_policy do |p| + p.default_src "https://example.com" + end + + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src https://example.com;" + end + + test "override content security policy to report only in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + content_security_policy_report_only + + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:;", report_only: true + end + + test "global content security policy added to rack app" do + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + + app = ->(env) { + [200, { "Content-Type" => "text/html" }, ["<p>Hello, World!</p>"]] + } + + root to: app + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:;" + end + + private + + def assert_policy(expected, report_only: false) + assert_equal 200, last_response.status + + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil last_response.headers[unexpected_header] + assert_equal expected, last_response.headers[expected_header] + end + end +end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index d2b77bd015..e631318f82 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -266,7 +266,7 @@ module ApplicationTests ActiveRecord::Base.connection RUBY require "#{app_path}/config/environment" - assert !ActiveRecord::Base.connection_pool.active_connection? + assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection? end end end diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb index 23b20d578c..c65c955734 100644 --- a/railties/test/application/initializers/notifications_test.rb +++ b/railties/test/application/initializers/notifications_test.rb @@ -32,6 +32,7 @@ module ApplicationTests logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new ActiveRecord::Base.logger = logger + ActiveRecord::Base.verbose_query_logs = false # Mimic Active Record notifications instrument "sql.active_record", name: "SQL", sql: "SHOW tables" diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index de1e240fd3..2632dd7cde 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -352,11 +352,11 @@ class LoadingTest < ActiveSupport::TestCase def test_initialize_can_be_called_at_any_time require "#{app_path}/config/application" - assert !Rails.initialized? - assert !Rails.application.initialized? + assert_not_predicate Rails, :initialized? + assert_not_predicate Rails.application, :initialized? Rails.initialize! - assert Rails.initialized? - assert Rails.application.initialized? + assert_predicate Rails, :initialized? + assert_predicate Rails.application, :initialized? end private diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 4e77cece1b..ba186bda44 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -296,7 +296,7 @@ module ApplicationTests test "mailer preview not found" do app("development") get "/rails/mailers/notifier" - assert last_response.not_found? + assert_predicate last_response, :not_found? assert_match "Mailer preview 'notifier' not found", last_response.body end @@ -326,7 +326,7 @@ module ApplicationTests app("development") get "/rails/mailers/notifier/bar" - assert last_response.not_found? + assert_predicate last_response, :not_found? assert_match "Email 'bar' not found in NotifierPreview", last_response.body end @@ -382,7 +382,7 @@ module ApplicationTests app("development") get "/rails/mailers/notifier/foo?part=text%2Fhtml" - assert last_response.not_found? + assert_predicate last_response, :not_found? assert_match "Email part 'text/html' not found in NotifierPreview#foo", last_response.body end @@ -450,11 +450,67 @@ module ApplicationTests get "/rails/mailers/notifier/foo.html" assert_equal 200, last_response.status - assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body + assert_match '<option selected value="part=text%2Fhtml">View as HTML email</option>', last_response.body get "/rails/mailers/notifier/foo.txt" assert_equal 200, last_response.status - assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body + assert_match '<option selected value="part=text%2Fplain">View as plain-text email</option>', last_response.body + end + + test "locale menu selects correct option" do + app_file "config/initializers/available_locales.rb", <<-RUBY + Rails.application.configure do + config.i18n.available_locales = %i[en ja] + end + RUBY + + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + html_template "notifier/foo", <<-RUBY + <p>Hello, World!</p> + RUBY + + text_template "notifier/foo", <<-RUBY + Hello, World! + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo.html" + assert_equal 200, last_response.status + assert_match '<option selected value="locale=en">en', last_response.body + assert_match '<option value="locale=ja">ja', last_response.body + + get "/rails/mailers/notifier/foo.html?locale=ja" + assert_equal 200, last_response.status + assert_match '<option value="locale=en">en', last_response.body + assert_match '<option selected value="locale=ja">ja', last_response.body + + get "/rails/mailers/notifier/foo.txt" + assert_equal 200, last_response.status + assert_match '<option selected value="locale=en">en', last_response.body + assert_match '<option value="locale=ja">ja', last_response.body + + get "/rails/mailers/notifier/foo.txt?locale=ja" + assert_equal 200, last_response.status + assert_match '<option value="locale=en">en', last_response.body + assert_match '<option selected value="locale=ja">ja', last_response.body end test "mailer previews create correct links when loaded on a subdirectory" do @@ -520,8 +576,8 @@ module ApplicationTests get "/rails/mailers/notifier/foo.txt" assert_equal 200, last_response.status assert_match '<iframe seamless name="messageBody" src="?part=text%2Fplain">', last_response.body - assert_match '<option selected value="?part=text%2Fplain">', last_response.body - assert_match '<option value="?part=text%2Fhtml">', last_response.body + assert_match '<option selected value="part=text%2Fplain">', last_response.body + assert_match '<option value="part=text%2Fhtml">', last_response.body get "/rails/mailers/notifier/foo?part=text%2Fplain" assert_equal 200, last_response.status @@ -530,8 +586,8 @@ module ApplicationTests get "/rails/mailers/notifier/foo.html?name=Ruby" assert_equal 200, last_response.status assert_match '<iframe seamless name="messageBody" src="?name=Ruby&part=text%2Fhtml">', last_response.body - assert_match '<option selected value="?name=Ruby&part=text%2Fhtml">', last_response.body - assert_match '<option value="?name=Ruby&part=text%2Fplain">', last_response.body + assert_match '<option selected value="name=Ruby&part=text%2Fhtml">', last_response.body + assert_match '<option value="name=Ruby&part=text%2Fplain">', last_response.body get "/rails/mailers/notifier/foo?name=Ruby&part=text%2Fhtml" assert_equal 200, last_response.status diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index 9822ec563d..3768d8ce2d 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -140,7 +140,7 @@ module ApplicationTests etag = last_response.headers["ETag"] get "/expires/expires_etag", { private: true }, { "HTTP_IF_NONE_MATCH" => etag } - assert_equal "miss", last_response.headers["X-Rack-Cache"] + assert_equal "miss", last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end @@ -174,7 +174,7 @@ module ApplicationTests last = last_response.headers["Last-Modified"] get "/expires/expires_last_modified", { private: true }, { "HTTP_IF_MODIFIED_SINCE" => last } - assert_equal "miss", last_response.headers["X-Rack-Cache"] + assert_equal "miss", last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 0a5a524692..b9e4a9ccc0 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -42,9 +42,11 @@ module ApplicationTests "ActionDispatch::Cookies", "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", + "ActionDispatch::ContentSecurityPolicy::Middleware", "Rack::Head", "Rack::ConditionalGet", - "Rack::ETag" + "Rack::ETag", + "Rack::TempfileReaper" ], middleware end @@ -68,7 +70,8 @@ module ApplicationTests "ActionDispatch::Callbacks", "Rack::Head", "Rack::ConditionalGet", - "Rack::ETag" + "Rack::ETag", + "Rack::TempfileReaper" ], middleware end @@ -248,7 +251,7 @@ module ApplicationTests test "can't change middleware after it's built" do boot! - assert_raise RuntimeError do + assert_raise frozen_error_class do app.config.middleware.use Rack::Config end end diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index e9bc91785c..10d3313f6e 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -59,7 +59,7 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase assert_equal 200, last_response.status values = ActionView::LookupContext::DetailsKey.digest_caches.first.values - assert_equal [ "8ba099b7749542fe765ff34a6824d548" ], values + assert_equal [ "effc8928d0b33535c8a21d24ec617161" ], values assert_equal %w(david dingus), last_response.body.split.map(&:strip) end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index fd22477539..5b4c42c189 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -26,12 +26,12 @@ module ApplicationTests FileUtils.rm_rf("#{app_path}/config/database.yml") end - def db_create_and_drop(expected_database) + def db_create_and_drop(expected_database, environment_loaded: true) Dir.chdir(app_path) do output = rails("db:create") assert_match(/Created database/, output) assert File.exist?(expected_database) - assert_equal expected_database, ActiveRecord::Base.connection_config[:database] + assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded output = rails("db:drop") assert_match(/Dropped database/, output) assert !File.exist?(expected_database) @@ -49,6 +49,22 @@ module ApplicationTests db_create_and_drop database_url_db_name end + test "db:create and db:drop respect environment setting" do + app_file "config/database.yml", <<-YAML + development: + database: <%= Rails.application.config.database %> + adapter: sqlite3 + YAML + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.database = "db/development.sqlite3" + end + RUBY + + db_create_and_drop "db/development.sqlite3", environment_loaded: false + end + def with_database_existing Dir.chdir(app_path) do set_database_url @@ -82,6 +98,20 @@ module ApplicationTests end end + test "db:create works when schema cache exists and database does not exist" do + use_postgresql + + begin + rails %w(db:create db:migrate db:schema:cache:dump) + + rails "db:drop" + rails "db:create" + assert_equal 0, $?.exitstatus + ensure + rails "db:drop" rescue nil + end + end + test "db:drop failure because database does not exist" do output = rails("db:drop:_unsafe", "--trace") assert_match(/does not exist/, output) @@ -173,7 +203,7 @@ module ApplicationTests rails "environment", "db:drop", "db:structure:load" assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" - #if structure is not loaded correctly, exception would be raised + # if structure is not loaded correctly, exception would be raised assert_equal 0, Book.count end end @@ -268,7 +298,7 @@ module ApplicationTests ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Base.establish_connection :test require "#{app_path}/app/models/book" - #if structure is not loaded correctly, exception would be raised + # if structure is not loaded correctly, exception would be raised assert_equal 0, Book.count assert_match ActiveRecord::Base.configurations["test"]["database"], ActiveRecord::Base.connection_config[:database] diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index 788f9160d6..bf5b07afbd 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -29,12 +29,32 @@ module ApplicationTests assert_match(/AMigration: migrated/, output) + # run all the migrations to test scope for down + output = rails("db:migrate") + assert_match(/CreateUsers: migrated/, output) + output = rails("db:migrate", "SCOPE=bukkits", "VERSION=0") assert_no_match(/drop_table\(:users\)/, output) assert_no_match(/CreateUsers/, output) assert_no_match(/remove_column\(:users, :email\)/, output) assert_match(/AMigration: reverted/, output) + + output = rails("db:migrate", "VERSION=0") + + assert_match(/CreateUsers: reverted/, output) + end + + test "version outputs current version" do + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + rails "db:migrate" + + output = rails("db:version") + assert_match(/Current version: 1/, output) end test "migrate with specified VERSION in different formats" do diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index bf89098645..5a6404bd0a 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -132,8 +132,14 @@ module ApplicationTests output = rails("routes") assert_equal <<-MESSAGE.strip_heredoc, output - Prefix Verb URI Pattern Controller#Action - cart GET /cart(.:format) cart#show + 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_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show + rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#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 @@ -168,14 +174,19 @@ module ApplicationTests output = rails("routes", "-g", "show") assert_equal <<-MESSAGE.strip_heredoc, output - Prefix Verb URI Pattern Controller#Action - cart GET /cart(.:format) cart#show + 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_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show + rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show MESSAGE output = rails("routes", "-g", "POST") assert_equal <<-MESSAGE.strip_heredoc, output - Prefix Verb URI Pattern Controller#Action - POST /cart(.:format) cart#create + Prefix Verb URI Pattern Controller#Action + POST /cart(.:format) cart#create + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create MESSAGE output = rails("routes", "-g", "basketballs") @@ -232,11 +243,13 @@ module ApplicationTests RUBY assert_equal <<-MESSAGE.strip_heredoc, rails("routes") - You don't have any routes defined! - - Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. + Prefix Verb URI Pattern Controller#Action + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show + rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#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 @@ -250,8 +263,14 @@ module ApplicationTests output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile routes` } assert_equal <<-MESSAGE.strip_heredoc, output - Prefix Verb URI Pattern Controller#Action - cart GET /cart(.:format) cart#show + 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_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show + rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#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 diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index aa5d495c97..8f5f48c281 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -107,13 +107,13 @@ module ApplicationTests def test_runner_detects_syntax_errors output = rails("runner", "puts 'hello world", allow_failure: true) - assert_not $?.success? + assert_not_predicate $?, :success? assert_match "unterminated string meets end of file", output end def test_runner_detects_bad_script_name output = rails("runner", "iuiqwiourowe", allow_failure: true) - assert_not $?.success? + assert_not_predicate $?, :success? assert_match "undefined local variable or method `iuiqwiourowe' for", output end diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb index 2238f4f63a..f3a7e00a4d 100644 --- a/railties/test/application/server_test.rb +++ b/railties/test/application/server_test.rb @@ -29,12 +29,17 @@ module ApplicationTests server.app log = File.read(Rails.application.config.paths["log"].first) - assert_match(/DEPRECATION WARNING: Use `Rails::Application` subclass to start the server is deprecated/, log) + assert_match(/DEPRECATION WARNING: Using `Rails::Application` subclass to start the server is deprecated/, log) end test "restart rails server with custom pid file path" do skip "PTY unavailable" unless available_pty? + File.open("#{app_path}/config/boot.rb", "w") do |f| + f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile.to_s}'" + f.puts "require 'bundler/setup'" + end + master, slave = PTY.open pid = nil diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index e92a0466dd..a01325fdb8 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -569,6 +569,40 @@ module ApplicationTests assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" end + def test_running_with_ruby_gets_test_env_by_default + # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to + # nil before we run the tests in the test app. + re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil + + file = create_test_for_env("test") + results = Dir.chdir(app_path) { + `ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line } + } + assert_equal 1, results.length + failures = results.first["failures"] + flunk(failures.first) unless failures.empty? + + ensure + ENV["RAILS_ENV"] = re + end + + def test_running_with_ruby_can_set_env_via_cmdline + # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to + # nil before we run the tests in the test app. + re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil + + file = create_test_for_env("development") + results = Dir.chdir(app_path) { + `RAILS_ENV=development ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line } + } + assert_equal 1, results.length + failures = results.first["failures"] + flunk(failures.first) unless failures.empty? + + ensure + ENV["RAILS_ENV"] = re + end + def test_rake_passes_multiple_TESTOPTS_to_minitest create_test_file :models, "account" output = Dir.chdir(app_path) { `bin/rake test TESTOPTS='-v --seed=1234'` } @@ -727,6 +761,45 @@ module ApplicationTests app_file "db/schema.rb", "" end + def create_test_for_env(env) + app_file "test/models/environment_test.rb", <<-RUBY + require 'test_helper' + class JSONReporter < Minitest::AbstractReporter + def record(result) + puts JSON.dump(klass: result.class.name, + name: result.name, + failures: result.failures, + assertions: result.assertions, + time: result.time) + end + end + + def Minitest.plugin_json_reporter_init(opts) + Minitest.reporter.reporters.clear + Minitest.reporter.reporters << JSONReporter.new + end + + Minitest.extensions << "rails" + Minitest.extensions << "json_reporter" + + # Minitest uses RubyGems to find plugins, and since RubyGems + # doesn't know about the Rails installation we're pointing at, + # Minitest won't require the Rails minitest plugin when we run + # these integration tests. So we have to manually require the + # Minitest plugin here. + require 'minitest/rails_plugin' + + class EnvironmentTest < ActiveSupport::TestCase + def test_environment + test_db = ActiveRecord::Base.configurations[#{env.dump}]["database"] + db_file = ActiveRecord::Base.connection_config[:database] + assert_match(test_db, db_file) + assert_equal #{env.dump}, ENV["RAILS_ENV"] + end + end + RUBY + end + def create_test_file(path = :unit, name = "test", pass: true) app_file "test/#{path}/#{name}_test.rb", <<-RUBY require 'test_helper' diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 45ab8d87ff..b7cdb8229e 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -20,30 +20,30 @@ class Rails::ConsoleTest < ActiveSupport::TestCase def test_sandbox_option console = Rails::Console.new(app, parse_arguments(["--sandbox"])) - assert console.sandbox? + assert_predicate console, :sandbox? end def test_short_version_of_sandbox_option console = Rails::Console.new(app, parse_arguments(["-s"])) - assert console.sandbox? + assert_predicate console, :sandbox? end def test_no_options console = Rails::Console.new(app, parse_arguments([])) - assert !console.sandbox? + assert_not_predicate console, :sandbox? end def test_start start - assert app.console.started? + assert_predicate app.console, :started? assert_match(/Loading \w+ environment \(Rails/, output) end def test_start_with_sandbox start ["--sandbox"] - assert app.console.started? + assert_predicate app.console, :started? assert app.sandbox assert_match(/Loading \w+ environment in sandbox \(Rails/, output) end diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb index 743fb5f788..663ee73bcd 100644 --- a/railties/test/commands/credentials_test.rb +++ b/railties/test/commands/credentials_test.rb @@ -13,7 +13,10 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase teardown { teardown_app } test "edit without editor gives hint" do - assert_match "No $EDITOR to open credentials in", run_edit_command(editor: "") + run_edit_command(editor: "").tap do |output| + assert_match "No $EDITOR to open file in", output + assert_match "bin/rails credentials:edit", output + end end test "edit credentials" do @@ -23,10 +26,6 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end - test "show credentials" do - assert_match(/access_key_id: 123/, run_show_command) - end - test "edit command does not add master key to gitignore when already exist" do run_edit_command @@ -36,6 +35,44 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end + test "edit command does not overwrite by default if credentials already exists" do + run_edit_command(editor: "eval echo api_key: abc >") + assert_match(/api_key: abc/, run_show_command) + + run_edit_command + assert_match(/api_key: abc/, run_show_command) + end + + test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do + Dir.chdir(app_path) do + key = IO.binread("config/master.key").strip + FileUtils.rm("config/master.key") + + switch_env("RAILS_MASTER_KEY", key) do + run_edit_command + assert_not File.exist?("config/master.key") + end + end + end + + test "show credentials" do + assert_match(/access_key_id: 123/, run_show_command) + end + + test "show command raise error when require_master_key is specified and key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = true" + + assert_match(/Missing encryption key to decrypt file with/, run_show_command(allow_failure: true)) + end + + test "show command does not raise error when require_master_key is false and master key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = false" + + assert_match(/Missing master key to decrypt credentials/, run_show_command) + end + private def run_edit_command(editor: "cat") switch_env("EDITOR", editor) do @@ -43,7 +80,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end - def run_show_command - rails "credentials:show" + def run_show_command(**options) + rails "credentials:show", **options end end diff --git a/railties/test/commands/encrypted_test.rb b/railties/test/commands/encrypted_test.rb new file mode 100644 index 0000000000..9fc73d5f18 --- /dev/null +++ b/railties/test/commands/encrypted_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" +require "rails/command" +require "rails/commands/encrypted/encrypted_command" + +class Rails::Command::EncryptedCommandTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup :build_app + teardown :teardown_app + + test "edit without editor gives hint" do + run_edit_command("config/tokens.yml.enc", editor: "").tap do |output| + assert_match "No $EDITOR to open file in", output + assert_match "bin/rails encrypted:edit", output + end + end + + test "edit encrypted file" do + # Run twice to ensure file can be reread after first edit pass. + 2.times do + assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc")) + end + end + + test "edit command does not add master key to gitignore when already exist" do + run_edit_command("config/tokens.yml.enc") + + Dir.chdir(app_path) do + assert_match "/config/master.key", File.read(".gitignore") + end + end + + test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do + Dir.chdir(app_path) do + key = IO.binread("config/master.key").strip + FileUtils.rm("config/master.key") + + switch_env("RAILS_MASTER_KEY", key) do + run_edit_command("config/tokens.yml.enc") + assert_not File.exist?("config/master.key") + end + end + end + + test "edit encrypts file with custom key" do + run_edit_command("config/tokens.yml.enc", key: "config/tokens.key") + + Dir.chdir(app_path) do + assert File.exist?("config/tokens.yml.enc") + assert File.exist?("config/tokens.key") + + assert_match "/config/tokens.key", File.read(".gitignore") + end + + assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")) + end + + test "show encrypted file with custom key" do + run_edit_command("config/tokens.yml.enc", key: "config/tokens.key") + + assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key")) + end + + test "show command raise error when require_master_key is specified and key does not exist" do + add_to_config "config.require_master_key = true" + + assert_match(/Missing encryption key to decrypt file with/, + run_show_command("config/tokens.yml.enc", key: "unexist.key", allow_failure: true)) + end + + test "show command does not raise error when require_master_key is false and master key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = false" + + assert_match(/Missing 'config\/master\.key' to decrypt data/, run_show_command("config/tokens.yml.enc")) + end + + test "won't corrupt encrypted file when passed wrong key" do + run_edit_command("config/tokens.yml.enc", key: "config/tokens.key") + + assert_match "passed the wrong key", + run_edit_command("config/tokens.yml.enc", allow_failure: true) + + assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key")) + end + + private + def run_edit_command(file, key: nil, editor: "cat", **options) + switch_env("EDITOR", editor) do + rails "encrypted:edit", prepare_args(file, key), **options + end + end + + def run_show_command(file, key: nil, **options) + rails "encrypted:show", prepare_args(file, key), **options + end + + def prepare_args(file, key) + args = [ file ] + args.push("--key", key) if key + args + end +end diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb index 66a81826e3..6b9f284a0c 100644 --- a/railties/test/commands/secrets_test.rb +++ b/railties/test/commands/secrets_test.rb @@ -8,21 +8,38 @@ require "rails/commands/secrets/secrets_command" class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation, EnvHelpers - def setup - build_app + setup :build_app + teardown :teardown_app + + test "edit without editor gives hint" do + assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "") end - def teardown - teardown_app + test "encrypted secrets are deprecated when using credentials" do + assert_match "Encrypted secrets is deprecated", run_setup_command + assert_equal 1, $?.exitstatus + assert_not File.exist?("config/secrets.yml.enc") end - test "edit without editor gives hint" do - assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "") + test "encrypted secrets are deprecated when running edit without setup" do + assert_match "Encrypted secrets is deprecated", run_setup_command + assert_equal 1, $?.exitstatus + assert_not File.exist?("config/secrets.yml.enc") + end + + test "encrypted secrets are deprecated for 5.1 config/secrets.yml apps" do + Dir.chdir(app_path) do + FileUtils.rm("config/credentials.yml.enc") + FileUtils.touch("config/secrets.yml") + + assert_match "Encrypted secrets is deprecated", run_setup_command + assert_equal 1, $?.exitstatus + assert_not File.exist?("config/secrets.yml.enc") + end end test "edit secrets" do - # Runs setup before first edit. - assert_match(/Adding config\/secrets\.yml\.key to store the encryption key/, run_edit_command) + prevent_deprecation # Run twice to ensure encrypted secrets can be reread after first edit pass. 2.times do @@ -31,22 +48,30 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase end test "show secrets" do - run_setup_command + prevent_deprecation + assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_show_command) end private + def prevent_deprecation + Dir.chdir(app_path) do + File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8") + File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==") + end + end + def run_edit_command(editor: "cat") switch_env("EDITOR", editor) do - rails "secrets:edit" + rails "secrets:edit", allow_failure: true end end def run_show_command - rails "secrets:show" + rails "secrets:show", allow_failure: true end def run_setup_command - rails "secrets:setup" + rails "secrets:setup", allow_failure: true end end diff --git a/railties/test/engine/test_test.rb b/railties/test/engine/test_test.rb new file mode 100644 index 0000000000..18af85a0aa --- /dev/null +++ b/railties/test/engine/test_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class Rails::Engine::TestTest < ActiveSupport::TestCase + setup do + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } + end + + teardown do + FileUtils.rm_rf(@destination_root) + end + + test "automatically synchronize test schema" do + Dir.chdir(plugin_path) do + # In order to confirm that migration files are loaded, generate multiple migration files. + `bin/rails generate model user name:string; + bin/rails generate model todo name:string; + RAILS_ENV=development bin/rails db:migrate` + + output = `bin/rails test test/models/bukkits/user_test.rb` + assert_includes(output, "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips") + end + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end +end diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb index 4bd8a07085..19379e200c 100644 --- a/railties/test/engine_test.rb +++ b/railties/test/engine_test.rb @@ -11,7 +11,7 @@ class EngineTest < ActiveSupport::TestCase end end - assert !engine.routes? + assert_not_predicate engine, :routes? end def test_application_can_be_subclassed diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 7791d472d8..4815cf6362 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -72,6 +72,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase assert_no_file "config/initializers/cookies_serializer.rb" assert_no_file "config/initializers/assets.rb" + assert_no_file "config/initializers/content_security_policy.rb" end def test_app_update_does_not_generate_unnecessary_bin_files @@ -149,6 +150,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase bin/yarn config/initializers/assets.rb config/initializers/cookies_serializer.rb + config/initializers/content_security_policy.rb lib/assets test/helpers tmp/cache/assets diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 78962ee30b..60b9ff1317 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -56,6 +56,7 @@ DEFAULT_APP_FILES = %w( config/initializers/assets.rb config/initializers/backtrace_silencers.rb config/initializers/cookies_serializer.rb + config/initializers/content_security_policy.rb config/initializers/filter_parameter_logging.rb config/initializers/inflections.rb config/initializers/mime_types.rb @@ -104,6 +105,15 @@ class AppGeneratorTest < Rails::Generators::TestCase ::DEFAULT_APP_FILES end + def test_skip_bundle + assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do + quietly { generator.invoke_all } + # skip_bundle is only about running bundle install, ensure the Gemfile is still + # generated. + assert_file "Gemfile" + end + end + def test_assets run_generator @@ -303,21 +313,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile", /^# gem 'mini_magick'/ end - def test_active_storage_install - command_check = -> command, _ do - @binstub_called ||= 0 - case command - when "active_storage:install" - @binstub_called += 1 - assert_equal 1, @binstub_called, "active_storage:install expected to be called once, but was called #{@install_called} times." - end - end - - generator.stub :rails_command, command_check do - quietly { generator.invoke_all } - end - end - def test_app_update_does_not_generate_active_storage_contents_when_skip_active_storage_is_given app_root = File.join(destination_root, "myapp") run_generator [app_root, "--skip-active-storage"] @@ -403,7 +398,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcmysql-adapter" else - assert_gem "mysql2", "'>= 0.3.18', '< 0.5'" + assert_gem "mysql2", "'~> 0.4.4'" end end @@ -418,7 +413,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcpostgresql-adapter" else - assert_gem "pg", "'~> 0.18'" + assert_gem "pg", "'>= 0.18', '< 2.0'" end end @@ -457,7 +452,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_defaults_to_puma_version run_generator [destination_root] - assert_gem "puma", "'~> 3.7'" + assert_gem "puma", "'~> 3.11'" end def test_generator_if_skip_puma_is_given @@ -742,6 +737,45 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_webpack_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." + end + end + + generator([destination_root], webpack: "webpack").stub(:rails_command, command_check) do + generator.stub :bundle_command, nil do + quietly { generator.invoke_all } + end + end + + assert_gem "webpacker" + end + + def test_webpack_option_with_js_framework + command_check = -> command, *_ do + case command + when "webpacker:install" + @webpacker ||= 0 + @webpacker += 1 + assert_equal 1, @webpacker, "webpacker:install expected to be called once, but was called #{@webpacker} times." + when "webpacker:install:react" + @react ||= 0 + @react += 1 + assert_equal 1, @react, "webpacker:install:react expected to be called once, but was called #{@react} times." + end + end + + generator([destination_root], webpack: "react").stub(:rails_command, command_check) do + generator.stub :bundle_command, nil do + quietly { generator.invoke_all } + end + end + end + def test_generator_if_skip_turbolinks_is_given run_generator [destination_root, "--skip-turbolinks"] @@ -756,6 +790,37 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_bootsnap + run_generator + + assert_gem "bootsnap" + assert_file "config/boot.rb" do |content| + assert_match(/require 'bootsnap\/setup'/, content) + end + end + + def test_skip_bootsnap + run_generator [destination_root, "--skip-bootsnap"] + + assert_file "Gemfile" do |content| + assert_no_match(/bootsnap/, content) + end + assert_file "config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) + end + end + + def test_bootsnap_with_dev_option + run_generator [destination_root, "--dev"] + + assert_file "Gemfile" do |content| + assert_no_match(/bootsnap/, content) + end + assert_file "config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) + end + end + def test_inclusion_of_ruby_version run_generator @@ -818,7 +883,7 @@ class AppGeneratorTest < Rails::Generators::TestCase template end - sequence = ["git init", "install", "exec spring binstub --all", "active_storage:install", "echo ran after_bundle"] + sequence = ["git init", "install", "exec spring binstub --all", "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}" @@ -835,7 +900,15 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - assert_equal 5, @sequence_step + assert_equal 4, @sequence_step + end + + def test_gitignore + run_generator + + assert_file ".gitignore" do |content| + assert_match(/config\/master\.key/, content) + end end def test_system_tests_directory_generated diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb index 3cb7fd6baa..2ae38045c5 100644 --- a/railties/test/generators/create_migration_test.rb +++ b/railties/test/generators/create_migration_test.rb @@ -70,7 +70,7 @@ class CreateMigrationTest < Rails::Generators::TestCase create_migration assert_match(/identical db\/migrate\/1_create_articles\.rb\n/, invoke!) - assert @migration.identical? + assert_predicate @migration, :identical? end def test_invoke_when_exists_not_identical diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb deleted file mode 100644 index eacb5166c0..0000000000 --- a/railties/test/generators/encrypted_secrets_generator_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require "generators/generators_test_helper" -require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" - -class EncryptedSecretsGeneratorTest < Rails::Generators::TestCase - include GeneratorsTestHelper - - def setup - super - cd destination_root - end - - def test_generates_key_file_and_encrypted_secrets_file - run_generator - - assert_file "config/secrets.yml.key", /\w+/ - - assert File.exist?("config/secrets.yml.enc") - assert_no_match(/# production:\n# external_api_key: \w+/, IO.binread("config/secrets.yml.enc")) - assert_match(/# production:\n# external_api_key: \w+/, Rails::Secrets.read) - end - - def test_appends_to_gitignore - FileUtils.touch(".gitignore") - - run_generator - - assert_file ".gitignore", /config\/secrets.yml.key/, /(?!config\/secrets.yml.enc)/ - end - - def test_warns_when_ignore_is_missing - assert_match(/Add this to your ignore file/i, run_generator) - end - - def test_doesnt_generate_a_new_key_file_if_already_opted_in_to_encrypted_secrets - FileUtils.mkdir("config") - File.open("config/secrets.yml.enc", "w") { |f| f.puts "already secrety" } - - run_generator - - assert_no_file "config/secrets.yml.key" - end -end diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb index c6f7bdeda1..772b4f6f0d 100644 --- a/railties/test/generators/generated_attribute_test.rb +++ b/railties/test/generators/generated_attribute_test.rb @@ -104,25 +104,25 @@ class GeneratedAttributeTest < Rails::Generators::TestCase def test_reference_is_true %w(references belongs_to).each do |attribute_type| - assert create_generated_attribute(attribute_type).reference? + assert_predicate create_generated_attribute(attribute_type), :reference? end end def test_reference_is_false %w(foo bar baz).each do |attribute_type| - assert !create_generated_attribute(attribute_type).reference? + assert_not_predicate create_generated_attribute(attribute_type), :reference? end end def test_polymorphic_reference_is_true %w(references belongs_to).each do |attribute_type| - assert create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic? + assert_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic? end end def test_polymorphic_reference_is_false %w(foo bar baz).each do |attribute_type| - assert !create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic? + assert_not_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic? end end @@ -148,7 +148,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index") assert_equal "supplier", att.name assert_equal :references, att.type - assert att.has_index? - assert att.required? + assert_predicate att, :has_index? + assert_predicate att, :required? end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index cb5d8da7b1..ad2a55f496 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -43,9 +43,9 @@ module GeneratorsTestHelper end def copy_routes - routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb", __dir__) + routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb.tt", __dir__) destination = File.join(destination_root, "config") FileUtils.mkdir_p(destination) - FileUtils.cp routes, destination + FileUtils.cp routes, File.join(destination, "routes.rb") end end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 49256883d6..fc7584c175 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -474,6 +474,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "test/dummy/Gemfile" assert_no_file "test/dummy/public/robots.txt" assert_no_file "test/dummy/README.md" + assert_no_file "test/dummy/config/master.key" + assert_no_file "test/dummy/config/credentials.yml.enc" assert_no_directory "test/dummy/lib/tasks" assert_no_directory "test/dummy/test" assert_no_directory "test/dummy/vendor" @@ -513,10 +515,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase gemfile_path = "#{Rails.root}/Gemfile" Object.const_set("APP_PATH", Rails.root) FileUtils.touch gemfile_path + File.write(gemfile_path, "#foo") run_generator - assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/ + assert_file gemfile_path, /^gem 'bukkits', path: 'tmp\/bukkits'/ ensure Object.send(:remove_const, "APP_PATH") FileUtils.rm gemfile_path @@ -718,6 +721,38 @@ class PluginGeneratorTest < Rails::Generators::TestCase Object.send(:remove_const, "ENGINE_ROOT") end + def test_after_bundle_callback + path = "http://example.org/rails_template" + template = %{ after_bundle { run "echo ran after_bundle" } }.dup + template.instance_eval "def read; self; end" # Make the string respond to read + + check_open = -> *args do + assert_equal [ path, "Accept" => "application/x-thor-template" ], args + template + end + + sequence = ["echo ran after_bundle"] + @sequence_step ||= 0 + ensure_bundler_first = -> command do + assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" + @sequence_step += 1 + end + + content = nil + generator([destination_root], template: path).stub(:open, check_open, template) do + generator.stub(:bundle_command, ensure_bundler_first) do + generator.stub(:run, ensure_bundler_first) do + silence_stream($stdout) do + content = capture(:stderr) { generator.invoke_all } + end + end + end + end + + assert_equal 1, @sequence_step + assert_match(/DEPRECATION WARNING: `after_bundle` is deprecated/, content) + end + private def action(*args, &block) diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index b6294c3b94..29426cd99f 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -471,8 +471,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/accounts/_form.html.erb" do |content| - assert_match(/^\W{4}<%= form\.text_field :name, id: :account_name %>/, content) - assert_match(/^\W{4}<%= form\.text_field :currency_id, id: :account_currency_id %>/, content) + assert_match(/^\W{4}<%= form\.text_field :name %>/, content) + assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content) end end @@ -495,8 +495,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/users/_form.html.erb" do |content| - assert_match(/<%= form\.password_field :password, id: :user_password %>/, content) - assert_match(/<%= form\.password_field :password_confirmation, id: :user_password_confirmation %>/, content) + assert_match(/<%= form\.password_field :password %>/, content) + assert_match(/<%= form\.password_field :password_confirmation %>/, content) end assert_file "app/views/users/index.html.erb" do |content| diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 6b746f3fed..97d43af60a 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -92,7 +92,9 @@ module SharedGeneratorTests end generator([destination_root], template: path).stub(:open, check_open, template) do - quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + generator.stub :bundle_command, nil do + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + end end end @@ -103,15 +105,6 @@ module SharedGeneratorTests end end - def test_skip_bundle - assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do - quietly { generator.invoke_all } - # skip_bundle is only about running bundle install, ensure the Gemfile is still - # generated. - assert_file "Gemfile" - end - end - def test_skip_git run_generator [destination_root, "--skip-git", "--full"] assert_no_file(".gitignore") @@ -252,7 +245,6 @@ module SharedGeneratorTests end assert_no_file "#{application_path}/config/storage.yml" - assert_no_directory "#{application_path}/db/migrate" assert_no_directory "#{application_path}/storage" assert_no_directory "#{application_path}/tmp/storage" @@ -283,7 +275,6 @@ module SharedGeneratorTests end assert_no_file "#{application_path}/config/storage.yml" - assert_no_directory "#{application_path}/db/migrate" assert_no_directory "#{application_path}/storage" assert_no_directory "#{application_path}/tmp/storage" diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 28e7617d7f..9f1f2a2289 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -36,6 +36,19 @@ class GeneratorsTest < Rails::Generators::TestCase assert_match "Maybe you meant 'migration'", output end + def test_generator_suggestions_except_en_locale + orig_available_locales = I18n.available_locales + orig_default_locale = I18n.default_locale + I18n.available_locales = :ja + I18n.default_locale = :ja + name = :tas + output = capture(:stdout) { Rails::Generators.invoke name } + assert_match "Maybe you meant 'task', 'job' or", output + ensure + I18n.available_locales = orig_available_locales + I18n.default_locale = orig_default_locale + end + def test_generator_multiple_suggestions name = :tas output = capture(:stdout) { Rails::Generators.invoke name } @@ -51,7 +64,7 @@ class GeneratorsTest < Rails::Generators::TestCase assert File.exist?(File.join(@path, "generators", "model_generator.rb")) assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do warnings = capture(:stderr) { Rails::Generators.invoke :model, ["Account"] } - assert warnings.empty? + assert_empty warnings end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 7522237a38..6568a356d6 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -82,22 +82,6 @@ module TestHelpers assert_match "charset=utf-8", resp[1]["Content-Type"] assert extract_body(resp).match(/Yay! You.*re on Rails!/) end - - def assert_success(resp) - assert_equal 202, resp[0] - end - - def assert_missing(resp) - assert_equal 404, resp[0] - end - - def assert_header(key, value, resp) - assert_equal value, resp[1][key.to_s] - end - - def assert_body(expected, resp) - assert_equal expected, extract_body(resp) - end end module Generation @@ -358,10 +342,12 @@ module TestHelpers end def app_file(path, contents, mode = "w") - FileUtils.mkdir_p File.dirname("#{app_path}/#{path}") - File.open("#{app_path}/#{path}", mode) do |f| + file_name = "#{app_path}/#{path}" + FileUtils.mkdir_p File.dirname(file_name) + File.open(file_name, mode) do |f| f.puts contents end + file_name end def remove_file(path) @@ -381,6 +367,21 @@ module TestHelpers $:.reject! { |path| path =~ %r'/(#{to_remove.join('|')})/' } end + + def use_postgresql + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: postgresql + pool: 5 + database: railties_test + development: + <<: *default + test: + <<: *default + YAML + end + end end end @@ -389,6 +390,10 @@ class ActiveSupport::TestCase include TestHelpers::Rack include TestHelpers::Generation include ActiveSupport::Testing::Stream + + def frozen_error_class + Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError + end end # Create a scope and build a fixture rails app diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 854b4448a4..9f5bb37c20 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -104,7 +104,7 @@ class PathsTest < ActiveSupport::TestCase File.stub(:exist?, true) do @root.add "app", with: "/app" @root["app"].autoload_once! - assert @root["app"].autoload_once? + assert_predicate @root["app"], :autoload_once? assert_includes @root.autoload_once, @root["app"].expanded.first end end @@ -112,17 +112,17 @@ class PathsTest < ActiveSupport::TestCase test "it is possible to remove a path that should be autoloaded only once" do @root["app"] = "/app" @root["app"].autoload_once! - assert @root["app"].autoload_once? + assert_predicate @root["app"], :autoload_once? @root["app"].skip_autoload_once! - assert !@root["app"].autoload_once? + assert_not_predicate @root["app"], :autoload_once? assert_not_includes @root.autoload_once, @root["app"].expanded.first end test "it is possible to add a path without assignment and specify it should be loaded only once" do File.stub(:exist?, true) do @root.add "app", with: "/app", autoload_once: true - assert @root["app"].autoload_once? + assert_predicate @root["app"], :autoload_once? assert_includes @root.autoload_once, "/app" end end @@ -130,7 +130,7 @@ class PathsTest < ActiveSupport::TestCase test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do File.stub(:exist?, true) do @root.add "app", with: ["/app", "/app2"], autoload_once: true - assert @root["app"].autoload_once? + assert_predicate @root["app"], :autoload_once? assert_includes @root.autoload_once, "/app" assert_includes @root.autoload_once, "/app2" end @@ -158,7 +158,7 @@ class PathsTest < ActiveSupport::TestCase File.stub(:exist?, true) do @root["app"] = "/app" @root["app"].eager_load! - assert @root["app"].eager_load? + assert_predicate @root["app"], :eager_load? assert_includes @root.eager_load, @root["app"].to_a.first end end @@ -166,17 +166,17 @@ class PathsTest < ActiveSupport::TestCase test "it is possible to skip a path from eager loading" do @root["app"] = "/app" @root["app"].eager_load! - assert @root["app"].eager_load? + assert_predicate @root["app"], :eager_load? @root["app"].skip_eager_load! - assert !@root["app"].eager_load? + assert_not_predicate @root["app"], :eager_load? assert_not_includes @root.eager_load, @root["app"].to_a.first end test "it is possible to add a path without assignment and mark it as eager" do File.stub(:exist?, true) do @root.add "app", with: "/app", eager_load: true - assert @root["app"].eager_load? + assert_predicate @root["app"], :eager_load? assert_includes @root.eager_load, "/app" end end @@ -184,7 +184,7 @@ class PathsTest < ActiveSupport::TestCase test "it is possible to add multiple paths without assignment and mark them as eager" do File.stub(:exist?, true) do @root.add "app", with: ["/app", "/app2"], eager_load: true - assert @root["app"].eager_load? + assert_predicate @root["app"], :eager_load? assert_includes @root.eager_load, "/app" assert_includes @root.eager_load, "/app2" end @@ -193,8 +193,8 @@ class PathsTest < ActiveSupport::TestCase test "it is possible to create a path without assignment and mark it both as eager and load once" do File.stub(:exist?, true) do @root.add "app", with: "/app", eager_load: true, autoload_once: true - assert @root["app"].eager_load? - assert @root["app"].autoload_once? + assert_predicate @root["app"], :eager_load? + assert_predicate @root["app"], :autoload_once? assert_includes @root.eager_load, "/app" assert_includes @root.autoload_once, "/app" end @@ -254,7 +254,7 @@ class PathsTest < ActiveSupport::TestCase test "a path can be added to the load path on creation" do File.stub(:exist?, true) do @root.add "app", with: "/app", load_path: true - assert @root["app"].load_path? + assert_predicate @root["app"], :load_path? assert_equal ["/app"], @root.load_paths end end @@ -271,7 +271,7 @@ class PathsTest < ActiveSupport::TestCase test "a path can be marked as autoload on creation" do File.stub(:exist?, true) do @root.add "app", with: "/app", autoload: true - assert @root["app"].autoload? + assert_predicate @root["app"], :autoload? assert_equal ["/app"], @root.autoload_paths end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index fc710feb63..a59c63f343 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -34,7 +34,7 @@ module RailtiesTest def migrations migration_root = File.expand_path(ActiveRecord::Migrator.migrations_paths.first, app_path) - ActiveRecord::Migrator.migrations(migration_root) + ActiveRecord::MigrationContext.new(migration_root).migrations end test "serving sprocket's assets" do @@ -980,14 +980,14 @@ YAML boot_rails app_generators = Rails.application.config.generators.options[:rails] - assert_equal :mongoid , app_generators[:orm] - assert_equal :liquid , app_generators[:template_engine] + assert_equal :mongoid, app_generators[:orm] + assert_equal :liquid, app_generators[:template_engine] assert_equal :test_unit, app_generators[:test_framework] generators = Bukkits::Engine.config.generators.options[:rails] assert_equal :data_mapper, generators[:orm] - assert_equal :haml , generators[:template_engine] - assert_equal :rspec , generators[:test_framework] + assert_equal :haml, generators[:template_engine] + assert_equal :rspec, generators[:test_framework] end test "engine should get default generators with ability to overwrite them" do @@ -1003,10 +1003,10 @@ YAML generators = Bukkits::Engine.config.generators.options[:rails] assert_equal :active_record, generators[:orm] - assert_equal :rspec , generators[:test_framework] + assert_equal :rspec, generators[:test_framework] app_generators = Rails.application.config.generators.options[:rails] - assert_equal :test_unit , app_generators[:test_framework] + assert_equal :test_unit, app_generators[:test_framework] end test "do not create table_name_prefix method if it already exists" do @@ -1479,6 +1479,21 @@ YAML assert_equal "/fruits/2/bukkits/posts", last_response.body end + test "active_storage:install task works within engine" do + @plugin.write "Rakefile", <<-RUBY + APP_RAKEFILE = '#{app_path}/Rakefile' + load 'rails/tasks/engine.rake' + RUBY + + Dir.chdir(@plugin.path) do + output = `bundle exec rake app:active_storage:install` + assert $?.success?, output + + active_storage_migration = migrations.detect { |migration| migration.name == "CreateActiveStorageTables" } + assert active_storage_migration + end + end + private def app Rails.application diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb index 888fee173a..06877bc76a 100644 --- a/railties/test/secrets_test.rb +++ b/railties/test/secrets_test.rb @@ -1,20 +1,13 @@ # frozen_string_literal: true require "isolation/abstract_unit" -require "rails/generators" -require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" require "rails/secrets" class Rails::SecretsTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation - def setup - build_app - end - - def teardown - teardown_app - end + setup :build_app + teardown :teardown_app test "setting read to false skips parsing" do run_secrets_generator do @@ -172,9 +165,8 @@ class Rails::SecretsTest < ActiveSupport::TestCase private def run_secrets_generator Dir.chdir(app_path) do - capture(:stdout) do - Rails::Generators::EncryptedSecretsGenerator.start - end + File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8") + File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==") add_to_config <<-RUBY config.read_encrypted_secrets = true diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index ad852d0f35..91cb47779b 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -163,7 +163,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase end def failed_test - ft = ExampleTest.new(:woot) + ft = Minitest::Result.from(ExampleTest.new(:woot)) ft.failures << begin raise Minitest::Assertion, "boo" rescue Minitest::Assertion => e @@ -176,17 +176,17 @@ class TestUnitReporterTest < ActiveSupport::TestCase error = ArgumentError.new("wups") error.set_backtrace([ "some_test.rb:4" ]) - et = ExampleTest.new(:woot) + et = Minitest::Result.from(ExampleTest.new(:woot)) et.failures << Minitest::UnexpectedError.new(error) et end def passing_test - ExampleTest.new(:woot) + Minitest::Result.from(ExampleTest.new(:woot)) end def skipped_test - st = ExampleTest.new(:woot) + st = Minitest::Result.from(ExampleTest.new(:woot)) st.failures << begin raise Minitest::Skip, "skipchurches, misstemples" rescue Minitest::Assertion => e diff --git a/version.rb b/version.rb index 92b5e0392a..54bfbdd516 100644 --- a/version.rb +++ b/version.rb @@ -7,8 +7,8 @@ module Rails end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" |