diff options
1502 files changed, 37818 insertions, 19240 deletions
diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..71704b3cd7 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,28 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 90 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - With reproduction steps + - attached PR + - regression + - release blocker +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not been commented on for at least three months. + + The resources of the Rails team are limited, and so we are asking for your help. + + If you can still reproduce this error on the `5-1-stable` branch or on `master`, + please reply with all of the information you have about it in order to keep the issue open. + + Thank you for all your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false +# Limit to only `issues` or `pulls` +only: issues diff --git a/.gitignore b/.gitignore index 9268977c2f..32939b7bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .Gemfile .ruby-version +.byebug_history debug.log pkg /.bundle @@ -19,3 +20,5 @@ pkg /railties/doc /railties/tmp /guides/output +node_modules/ +/actionview/log diff --git a/.rubocop.yml b/.rubocop.yml index 139e6a7efb..8a0c55d5d4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,12 @@ AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.2 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop # to ignore them, so only the ones explicitly set in this file are enabled. DisabledByDefault: true Exclude: - '**/templates/**/*' - '**/vendor/**/*' + - 'actionpack/lib/action_dispatch/journey/parser.rb' # Prefer &&/|| over and/or. Style/AndOr: @@ -17,23 +18,27 @@ Style/BracesAroundHashParameters: Enabled: true # Align `when` with `case`. -Style/CaseIndentation: +Layout/CaseIndentation: Enabled: true # Align comments with method definitions. -Style/CommentIndentation: +Layout/CommentIndentation: Enabled: true # No extra empty lines. -Style/EmptyLines: +Layout/EmptyLines: Enabled: true # In a regular class definition, no empty lines around the body. -Style/EmptyLinesAroundClassBody: +Layout/EmptyLinesAroundClassBody: + Enabled: true + +# In a regular method definition, no empty lines around the body. +Layout/EmptyLinesAroundMethodBody: Enabled: true # In a regular module definition, no empty lines around the body. -Style/EmptyLinesAroundModuleBody: +Layout/EmptyLinesAroundModuleBody: Enabled: true # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. @@ -42,28 +47,49 @@ Style/HashSyntax: # Method definitions after `private` or `protected` isolated calls need one # extra level of indentation. -Style/IndentationConsistency: +Layout/IndentationConsistency: Enabled: true EnforcedStyle: rails # Two spaces, no tabs (for indentation). -Style/IndentationWidth: +Layout/IndentationWidth: + Enabled: true + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: Enabled: true +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + # Defining a method with parameters needs parentheses. Style/MethodDefParentheses: Enabled: true # Use `foo {}` not `foo{}`. -Style/SpaceBeforeBlockBraces: +Layout/SpaceBeforeBlockBraces: Enabled: true # Use `foo { bar }` not `foo {bar}`. -Style/SpaceInsideBlockBraces: +Layout/SpaceInsideBlockBraces: Enabled: true # Use `{ a: 1 }` not `{a:1}`. -Style/SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: Enabled: true # Check quotes usage according to lint rule below. @@ -72,15 +98,15 @@ Style/StringLiterals: EnforcedStyle: double_quotes # Detect hard tabs, no hard tabs. -Style/Tab: +Layout/Tab: Enabled: true # Blank lines should not have any spaces. -Style/TrailingBlankLines: +Layout/TrailingBlankLines: Enabled: true # No trailing whitespace. -Style/TrailingWhitespace: +Layout/TrailingWhitespace: Enabled: true # Use quotes for string literals when they are enough. @@ -91,7 +117,7 @@ Style/UnneededPercentQ: # assignments, where it should be aligned with the LHS. Lint/EndAlignment: Enabled: true - AlignWith: variable + EnforcedStyleAlignWith: variable # 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 f01b58ecb3..de708c509c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,12 @@ cache: directories: - /tmp/cache/unicode_conformance - /tmp/beanstalkd-1.10 + - node_modules + - $HOME/.nvm services: - memcached - redis - - rabbitmq addons: postgresql: "9.4" @@ -18,12 +19,17 @@ addons: bundler_args: --without test --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" + - "gem update --system" + - "gem update 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" + - "[[ $GEM != 'av:ujs' ]] || nvm install node" + - "[[ $GEM != 'av:ujs' ]] || node --version" + - "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)" + - "[[ $GEM != 'av:ujs' ]] || [[ $(phantomjs --version) > '2' ]] || npm install -g phantomjs-prebuilt" -before_script: - - bundle update +before_script: # Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted. # Decodes to e.g. `export VARIABLE=VALUE` - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4") @@ -32,44 +38,78 @@ before_script: script: 'ci/travis.rb' env: + global: + - "JRUBY_OPTS='--dev -J-Xmx1024M'" matrix: - "GEM=railties" - - "GEM=ap" - - "GEM=ac" - - "GEM=ac FAYE=1" - - "GEM=ac:integration" - - "GEM=ac:integration FAYE=1" + - "GEM=ap,ac" - "GEM=am,amo,as,av,aj" - "GEM=as PRESERVE_TIMEZONES=1" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" - - "GEM=aj:integration" - "GEM=guides" + - "GEM=ac:integration" rvm: - - 2.2.5 - - 2.3.1 + - 2.2.7 + - 2.3.4 + - 2.4.1 - ruby-head matrix: include: - # Latest compiled version in http://rubies.travis-ci.org - - rvm: 2.3.1 + - rvm: 2.4.1 + env: "GEM=av:ujs" + - rvm: 2.2.7 + env: "GEM=aj:integration" + services: + - memcached + - redis + - rabbitmq + - rvm: 2.3.4 + env: "GEM=aj:integration" + services: + - memcached + - redis + - rabbitmq + - rvm: 2.4.1 + env: "GEM=aj:integration" + services: + - memcached + - redis + - rabbitmq + - rvm: ruby-head + env: "GEM=aj:integration" + services: + - memcached + - redis + - rabbitmq + - rvm: 2.3.4 env: - - "GEM=ar:mysql2" + - "GEM=ar:mysql2 MYSQL=mariadb" addons: mariadb: 10.0 - - rvm: jruby-9.0.5.0 + - rvm: 2.3.4 + env: + - "GEM=ar:sqlite3_mem" + - rvm: 2.3.4 + env: + - "GEM=ar:postgresql POSTGRES=9.2" + addons: + postgresql: "9.2" + - rvm: jruby-9.1.10.0 + jdk: oraclejdk8 + env: + - "GEM=ap" + - rvm: jruby-9.1.10.0 jdk: oraclejdk8 env: - - "JRUBY_OPTS='--dev -J-Xmx1024M'" - - "GEM='ap'" + - "GEM=am,amo,aj" allow_failures: - rvm: ruby-head - - rvm: jruby-9.0.5.0 + - rvm: jruby-9.1.10.0 - env: "GEM=ac:integration" - - env: "GEM=ac:integration FAYE=1" fast_finish: true notifications: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6ebef7e89..b44486c75a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,9 @@ #### **Did you find a bug?** +* **Do not open up a GitHub issue if the bug is a security vulnerability + in Rails**, and instead to refer to our [security policy](http://rubyonrails.org/security/). + * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues). * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/rails/rails/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. @@ -7,6 +7,8 @@ end gemspec +gem "arel", github: "rails/arel" + # We need a newish Rake since Active Job sets its test tasks' descriptions. gem "rake", ">= 11.1" @@ -14,6 +16,8 @@ gem "rake", ">= 11.1" # be loaded after loading the test library. gem "mocha", "~> 0.14", require: false +gem "capybara", "~> 2.13" + gem "rack-cache", "~> 1.2" gem "jquery-rails" gem "coffee-rails" @@ -29,30 +33,36 @@ gem "bcrypt", "~> 3.1.11", require: false # sprockets. gem "uglifier", ">= 1.3.0", require: false -# Track stable branch of sass because it doesn't have circular require warnings. -gem "sass", github: "sass/sass", branch: "stable", require: false +# FIXME: Pending rb-inotify 0.9.9 release +gem "rb-inotify", github: "guard/rb-inotify", branch: "master", require: false + +# Explicitly avoid 1.x that doesn't support Ruby 2.4+ +gem "json", ">= 2.0.0" -# FIXME: Remove this fork after https://github.com/nex3/rb-inotify/pull/49 is fixed. -gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false +gem "rubocop", ">= 0.47", require: false group :doc do - gem "sdoc", "~> 0.4.0" + gem "sdoc", "> 1.0.0.rc1", "< 2.0" gem "redcarpet", "~> 3.2.3", platforms: :ruby gem "w3c_validators" - gem "kindlerb", "0.1.1" + gem "kindlerb", "~> 1.2.0" end # Active Support. gem "dalli", ">= 2.2.1" -gem "listen", "~> 3.0.5", require: false +gem "listen", ">= 3.0.5", "< 3.2", require: false +gem "libxml-ruby", platforms: :ruby + +# Action View. For testing Erubis handler deprecation. +gem "erubis", "~> 2.7.0", require: false # Active Job. group :job do - gem "resque", github: "resque/resque", require: false + gem "resque", require: false gem "resque-scheduler", require: false gem "sidekiq", require: false gem "sucker_punch", require: false - gem "delayed_job", require: false, github: "collectiveidea/delayed_job" + gem "delayed_job", require: false gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby gem "sneakers", require: false gem "que", require: false @@ -60,7 +70,7 @@ group :job do #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, github: "collectiveidea/delayed_job_active_record" + gem "delayed_job_active_record", require: false gem "sequel", require: false end @@ -72,17 +82,15 @@ group :cable do gem "hiredis", require: false gem "redis", require: false - gem "faye-websocket", require: false - - # Lock to 1.1.1 until the fix for https://github.com/faye/faye/issues/394 is released - gem "faye", "1.1.1", require: false + gem "websocket-client-simple", github: "matthewd/websocket-client-simple", branch: "close-race", require: false gem "blade", require: false, platforms: [:ruby] gem "blade-sauce_labs_plugin", require: false, platforms: [:ruby] + gem "sprockets-export", require: false end # Add your own local bundler stuff. -local_gemfile = File.dirname(__FILE__) + "/.Gemfile" +local_gemfile = File.expand_path(".Gemfile", __dir__) instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 68c75997b1..d08ddaf1b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,107 +1,89 @@ GIT remote: https://github.com/QueueClassic/queue_classic.git - revision: 1ef197b9db8149a895e59077badcb5b94d4c8b44 + revision: 51d56ca6fa2fdf1eeffdffd702ae1cc0940b5156 branch: master specs: queue_classic (3.2.0.RC1) - pg (>= 0.17, < 0.19) + pg (>= 0.17, < 0.20) GIT - remote: https://github.com/collectiveidea/delayed_job.git - revision: e3772d4f0c8470d0fcba00c86ca3bc4f5e876830 - specs: - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) - -GIT - remote: https://github.com/collectiveidea/delayed_job_active_record.git - revision: 36f434c4fd660e8f11ce932be117e9c71dde7212 - specs: - delayed_job_active_record (4.1.1) - activerecord (>= 3.0, < 5.1) - delayed_job (>= 3.0, < 5) - -GIT - remote: https://github.com/matthewd/rb-inotify.git - revision: 90553518d1fb79aedc98a3036c59bd2b6731ac40 - branch: close-handling + remote: https://github.com/guard/rb-inotify.git + revision: 7e3c714a09ae2b38d2620835e794150d8857cd49 + branch: master specs: - rb-inotify (0.9.7) - ffi (>= 0.5.0) + rb-inotify (0.9.9) + ffi (~> 1.0) GIT - remote: https://github.com/resque/resque.git - revision: a3a66389618b830de0e6acf862b0dc9fde05cf49 + remote: https://github.com/matthewd/websocket-client-simple.git + revision: e161305f1a466b9398d86df3b1731b03362da91b + branch: close-race specs: - resque (1.27.0) - mono_logger (~> 1.0) - multi_json (~> 1.0) - redis-namespace (~> 1.3) - sinatra (>= 0.9.2) - vegas (~> 0.1.2) + websocket-client-simple (0.3.0) + event_emitter + websocket GIT - remote: https://github.com/sass/sass.git - revision: 7716e67f3507c6f65878c69aa49ec358ebf675c7 - branch: stable + remote: https://github.com/rails/arel.git + revision: 5db56a513286814991c27000af2c0243cc19d1e2 specs: - sass (3.4.22) + arel (8.0.0) PATH remote: . specs: - actioncable (5.1.0.alpha) - actionpack (= 5.1.0.alpha) - nio4r (~> 1.2) + actioncable (5.2.0.alpha) + actionpack (= 5.2.0.alpha) + nio4r (~> 2.0) websocket-driver (~> 0.6.1) - actionmailer (5.1.0.alpha) - actionpack (= 5.1.0.alpha) - actionview (= 5.1.0.alpha) - activejob (= 5.1.0.alpha) + actionmailer (5.2.0.alpha) + actionpack (= 5.2.0.alpha) + actionview (= 5.2.0.alpha) + activejob (= 5.2.0.alpha) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.0.alpha) - actionview (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + actionpack (5.2.0.alpha) + actionview (= 5.2.0.alpha) + activesupport (= 5.2.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.1.0.alpha) - activesupport (= 5.1.0.alpha) + actionview (5.2.0.alpha) + activesupport (= 5.2.0.alpha) builder (~> 3.1) - erubis (~> 2.7.0) + erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (5.1.0.alpha) - activesupport (= 5.1.0.alpha) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.2.0.alpha) + activesupport (= 5.2.0.alpha) globalid (>= 0.3.6) - activemodel (5.1.0.alpha) - activesupport (= 5.1.0.alpha) - activerecord (5.1.0.alpha) - activemodel (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) - arel (~> 7.0) - activesupport (5.1.0.alpha) + 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 (~> 8.0) + activesupport (5.2.0.alpha) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - rails (5.1.0.alpha) - actioncable (= 5.1.0.alpha) - actionmailer (= 5.1.0.alpha) - actionpack (= 5.1.0.alpha) - actionview (= 5.1.0.alpha) - activejob (= 5.1.0.alpha) - activemodel (= 5.1.0.alpha) - activerecord (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + 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) + activesupport (= 5.2.0.alpha) bundler (>= 1.3.0, < 2.0) - railties (= 5.1.0.alpha) + railties (= 5.2.0.alpha) sprockets-rails (>= 2.0.0) - railties (5.1.0.alpha) - actionpack (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + railties (5.2.0.alpha) + actionpack (= 5.2.0.alpha) + activesupport (= 5.2.0.alpha) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -109,39 +91,47 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.4.0) - amq-protocol (2.0.1) - arel (7.1.2) - backburner (1.3.0) + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + amq-protocol (2.1.0) + ast (2.3.0) + backburner (1.3.1) beaneater (~> 1.0) + concurrent-ruby (~> 1.0.1) dante (> 0.1.5) bcrypt (3.1.11) bcrypt (3.1.11-x64-mingw32) bcrypt (3.1.11-x86-mingw32) beaneater (1.0.0) benchmark-ips (2.7.2) - blade (0.5.6) + blade (0.7.0) activesupport (>= 3.0.0) - blade-qunit_adapter (~> 1.20.0) + blade-qunit_adapter (~> 2.0.1) coffee-script coffee-script-source curses (~> 1.0.0) eventmachine faye sprockets (>= 3.0) - sprockets-export (~> 0.9.1) thin (>= 1.6.0) thor (~> 0.19.1) useragent (~> 0.16.7) - blade-qunit_adapter (1.20.0) - blade-sauce_labs_plugin (0.5.3) + blade-qunit_adapter (2.0.1) + blade-sauce_labs_plugin (0.6.2) childprocess faraday selenium-webdriver - builder (3.2.2) - bunny (2.2.2) + builder (3.2.3) + bunny (2.6.2) amq-protocol (>= 2.0.1) - byebug (9.0.5) + byebug (9.0.6) + capybara (2.14.0) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) childprocess (0.5.9) ffi (~> 1.0, >= 1.0.11) coffee-rails (4.2.1) @@ -150,14 +140,19 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.10.0) - concurrent-ruby (1.0.2) - connection_pool (2.2.0) + coffee-script-source (1.12.2) + concurrent-ruby (1.0.5) + connection_pool (2.2.1) cookiejar (0.3.3) curses (1.0.2) daemons (1.2.4) dalli (2.7.6) dante (0.2.0) + delayed_job (4.1.3) + activesupport (>= 3.0, < 5.2) + delayed_job_active_record (4.1.2) + activerecord (>= 3.0, < 5.2) + delayed_job (>= 3.0, < 5) em-hiredis (0.3.1) eventmachine (~> 1.0) hiredis (~> 0.6.0) @@ -169,14 +164,16 @@ GEM http_parser.rb (>= 0.6.0) em-socksify (0.3.1) eventmachine (>= 1.0.0.beta.4) + erubi (1.6.0) erubis (2.7.0) - eventmachine (1.2.0.1) - eventmachine (1.2.0.1-x64-mingw32) - eventmachine (1.2.0.1-x86-mingw32) + event_emitter (0.2.5) + eventmachine (1.2.1) + eventmachine (1.2.1-x64-mingw32) + eventmachine (1.2.1-x86-mingw32) execjs (2.7.0) - faraday (0.9.2) + faraday (0.11.0) multipart-post (>= 1.2, < 3) - faye (1.1.1) + faye (1.2.3) cookiejar (>= 0.3.0) em-http-request (>= 0.3.0) eventmachine (>= 0.12.0) @@ -184,31 +181,33 @@ GEM multi_json (>= 1.0.0) rack (>= 1.0.0) websocket-driver (>= 0.5.1) - faye-websocket (0.10.4) + faye-websocket (0.10.5) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.14) - ffi (1.9.14-x64-mingw32) - ffi (1.9.14-x86-mingw32) - globalid (0.3.7) - activesupport (>= 4.1.0) + ffi (1.9.17) + ffi (1.9.17-x64-mingw32) + ffi (1.9.17-x86-mingw32) + globalid (0.4.0) + activesupport (>= 4.2.0) hiredis (0.6.1) http_parser.rb (0.6.0) - i18n (0.7.0) - jquery-rails (4.2.1) + i18n (0.8.1) + jquery-rails (4.2.2) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (1.8.3) - kindlerb (0.1.1) + json (2.0.3) + kindlerb (1.2.0) mustache nokogiri - listen (3.0.8) + libxml-ruby (2.9.0) + listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.4) + mail (2.6.5) mime-types (>= 1.16, < 4) metaclass (0.0.4) method_source (0.8.2) @@ -223,25 +222,26 @@ GEM multi_json (1.12.1) multipart-post (2.0.0) mustache (1.0.3) - mysql2 (0.4.4) - mysql2 (0.4.4-x64-mingw32) - mysql2 (0.4.4-x86-mingw32) - nio4r (1.2.1) - nokogiri (1.6.8) + mysql2 (0.4.6) + mysql2 (0.4.6-x64-mingw32) + mysql2 (0.4.6-x86-mingw32) + nio4r (2.0.0) + nokogiri (1.7.1) mini_portile2 (~> 2.1.0) - pkg-config (~> 1.1.7) - nokogiri (1.6.8-x64-mingw32) + nokogiri (1.7.1-x64-mingw32) mini_portile2 (~> 2.1.0) - pkg-config (~> 1.1.7) - nokogiri (1.6.8-x86-mingw32) + nokogiri (1.7.1-x86-mingw32) mini_portile2 (~> 2.1.0) - pkg-config (~> 1.1.7) - pg (0.18.4) - pg (0.18.4-x64-mingw32) - pg (0.18.4-x86-mingw32) - pkg-config (1.1.7) - psych (2.1.1) - puma (3.6.0) + parallel (1.11.2) + parser (2.4.0.0) + ast (~> 2.2) + pg (0.19.0) + pg (0.19.0-x64-mingw32) + pg (0.19.0-x86-mingw32) + powerpack (0.1.1) + psych (2.2.2) + public_suffix (2.0.5) + puma (3.7.0) qu (0.2.0) multi_json qu-redis (0.2.0) @@ -250,64 +250,81 @@ GEM simple_uuid que (0.12.0) racc (1.4.14) - rack (2.0.1) + rack (2.0.3) rack-cache (1.6.1) rack (>= 0.4) - rack-protection (1.5.3) + rack-protection (2.0.0) rack rack-test (0.6.3) rack (>= 1.0) - rails-dom-testing (2.0.1) + rails-dom-testing (2.0.2) activesupport (>= 4.2.0, < 6.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rake (11.2.2) - rb-fsevent (0.9.7) - rdoc (4.2.2) - json (~> 1.4) + rainbow (2.2.2) + rake + rake (12.0.0) + rb-fsevent (0.9.8) + rdoc (5.1.0) redcarpet (3.2.3) - redis (3.3.1) + redis (3.3.3) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) + resque (1.27.0) + mono_logger (~> 1.0) + multi_json (~> 1.0) + redis-namespace (~> 1.3) + sinatra (>= 0.9.2) + vegas (~> 0.1.2) resque-scheduler (4.3.0) mono_logger (~> 1.0) redis (~> 3.3) resque (~> 1.26) rufus-scheduler (~> 3.2) + rubocop (0.49.1) + parallel (~> 1.10) + parser (>= 2.3.3.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + ruby-progressbar (1.8.1) + ruby_dep (1.5.0) rubyzip (1.2.0) - rufus-scheduler (3.2.2) + rufus-scheduler (3.3.2) + tzinfo + sass (3.4.23) 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) - sdoc (0.4.1) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) - selenium-webdriver (2.53.4) + sdoc (1.0.0.rc2) + rdoc (~> 5.0) + selenium-webdriver (3.0.5) childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) - sequel (4.38.0) + sequel (4.42.1) serverengine (1.5.11) sigdump (~> 0.2.2) - sidekiq (4.2.0) + sidekiq (5.0.0) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - rack-protection (~> 1.5) - redis (~> 3.2, >= 3.2.1) + rack-protection (>= 1.5.0) + redis (~> 3.3, >= 3.3.3) sigdump (0.2.4) simple_uuid (0.4.0) sinatra (1.0) rack (>= 1.0) - sneakers (2.3.5) - bunny (~> 2.2.0) + sneakers (2.4.0) + bunny (~> 2.6) serverengine (~> 1.5.11) thor thread (~> 0.1.7) - sprockets (3.7.0) + sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-export (0.9.1) @@ -315,40 +332,43 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.3.11) - sqlite3 (1.3.11-x64-mingw32) - sqlite3 (1.3.11-x86-mingw32) - stackprof (0.2.9) + sqlite3 (1.3.13) + sqlite3 (1.3.13-x64-mingw32) + sqlite3 (1.3.13-x86-mingw32) + stackprof (0.2.10) sucker_punch (2.0.2) concurrent-ruby (~> 1.0.0) thin (1.7.0) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (0.19.1) + thor (0.19.4) thread (0.1.7) - thread_safe (0.3.5) + thread_safe (0.3.6) tilt (2.0.5) turbolinks (5.0.1) turbolinks-source (~> 5) turbolinks-source (5.0.0) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) - tzinfo-data (1.2016.6) + tzinfo-data (1.2016.10) tzinfo (>= 1.0.0) - uglifier (3.0.2) + uglifier (3.0.4) execjs (>= 0.3.0, < 3) + unicode-display_width (1.2.1) useragent (0.16.8) vegas (0.1.11) rack (>= 1.0.0) - w3c_validators (1.2) - json - nokogiri + w3c_validators (1.3.1) + json (~> 2.0) + nokogiri (~> 1.6) wdm (0.1.1) websocket (1.2.3) websocket-driver (0.6.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -359,23 +379,26 @@ DEPENDENCIES activerecord-jdbcmysql-adapter (>= 1.3.0) activerecord-jdbcpostgresql-adapter (>= 1.3.0) activerecord-jdbcsqlite3-adapter (>= 1.3.0) + arel! backburner bcrypt (~> 3.1.11) benchmark-ips blade blade-sauce_labs_plugin byebug + capybara (~> 2.13) coffee-rails dalli (>= 2.2.1) - delayed_job! - delayed_job_active_record! + delayed_job + delayed_job_active_record em-hiredis - faye (= 1.1.1) - faye-websocket + erubis (~> 2.7.0) hiredis jquery-rails - kindlerb (= 0.1.1) - listen (~> 3.0.5) + json (>= 2.0.0) + kindlerb (~> 1.2.0) + libxml-ruby + listen (>= 3.0.5, < 3.2) minitest (< 5.3.4) mocha (~> 0.14) mysql2 (>= 0.4.4) @@ -393,14 +416,15 @@ DEPENDENCIES rb-inotify! redcarpet (~> 3.2.3) redis - resque! + resque resque-scheduler - sass! + rubocop (>= 0.47) sass-rails - sdoc (~> 0.4.0) + sdoc (> 1.0.0.rc1, < 2.0) sequel sidekiq sneakers + sprockets-export sqlite3 (~> 1.3.6) stackprof sucker_punch @@ -409,6 +433,7 @@ DEPENDENCIES uglifier (>= 1.3.0) w3c_validators wdm (>= 0.1.0) + websocket-client-simple! BUNDLED WITH - 1.13.1 + 1.15.0 diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000000..6b3cead1a7 --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2005-2017 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/RAILS_VERSION b/RAILS_VERSION index 8ea1016081..d5ebf861d3 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -5.1.0.alpha +5.2.0.alpha @@ -71,13 +71,17 @@ and may also be used independently outside Rails. * [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html) * [Ruby on Rails Guides](http://guides.rubyonrails.org) * [The API Documentation](http://api.rubyonrails.org) - * [Ruby on Rails Tutorial](http://www.railstutorial.org/book) + * [Ruby on Rails Tutorial](https://www.railstutorial.org/book) ## Contributing We encourage you to contribute to Ruby on Rails! Please check out the [Contributing to Ruby on Rails guide](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](http://contributors.rubyonrails.org) +Trying to report a possible security vulnerability in Rails? Please +check out our [security policy](http://rubyonrails.org/security/) for +guidelines about how to proceed. + Everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). ## Code Status diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index 4e6b811d3e..6e27d8ee5a 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -58,7 +58,7 @@ lists: Implementors will love you and help you. -### 3 Days before release +## 3 Days before release This is when you should release the release candidate. Here are your tasks for today: @@ -103,17 +103,19 @@ branch. Run `rake install` to generate the gems and install them locally. Then try generating a new app and ensure that nothing explodes. -Verify that Action Cable's package.json is updated with the RC version. +Verify that Action Cable and Action View's package.json files are updated with +the RC version. This will stop you from looking silly when you push an RC to rubygems.org and then realize it is broken. ### Release to RubyGems and NPM. -IMPORTANT: The Action Cable client is released as an NPM package, so you must -have Node.js installed, have an NPM account (npmjs.com), and be an actioncable -package owner (`npm owner ls actioncable`) to do a full release. Do not release -until you're set up with NPM! +IMPORTANT: The Action Cable client and Action View's UJS adapter are released +as NPM packages, so you must have Node.js installed, have an NPM account +(npmjs.com), and be a package owner for `actioncable` and `rails-ujs` (you can +check this via `npm owner ls actioncable` and `npm owner ls rails-ujs`) in +order to do a full release. Do not release until you're set up with NPM! Run `rake release`. This will populate the gemspecs and NPM package.json with the current RAILS_VERSION, commit the changes, tag it, and push the gems to @@ -179,7 +181,7 @@ more explanation on a particular step, see the RC steps. Today, do this stuff in this order: * Apply security patches to the release branch -* Update CHANGELOG with security fixes. +* Update CHANGELOG with security fixes * Update RAILS_VERSION to remove the rc * Build and test the gem * Release the gems @@ -206,7 +208,7 @@ so we need to give them the security fixes in patch form. * Blog announcements * Twitter announcements -* Merge the release branch to the stable branch. +* Merge the release branch to the stable branch * Drink beer (or other cocktail) ## Misc @@ -1,6 +1,6 @@ require "net/http" -$:.unshift File.expand_path("..", __FILE__) +$:.unshift __dir__ require "tasks/release" require "railties/lib/rails/api/task" @@ -23,9 +23,6 @@ task default: %w(test test:isolated) FRAMEWORKS.each do |project| system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project end - if task_name =~ /test/ - system(%(cd actioncable && env FAYE=1 #{$0} #{task_name} --trace)) || errors << "actioncable-faye" - end fail("Errors in #{errors.join(', ')}") unless errors.empty? end end @@ -36,7 +33,6 @@ task :smoke do system %(cd #{project} && #{$0} test:isolated --trace) end system %(cd activerecord && #{$0} sqlite3:isolated_test --trace) - system %(cd actioncable && env FAYE=1 #{$0} test:isolated --trace) end desc "Install gems for all projects." diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index dec6f7c027..2bf11da463 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,17 +1,8 @@ -* Protect against concurrent writes to a websocket connection from - multiple threads; the underlying OS write is not always threadsafe. +* ActionCable socket errors are now logged to the console - *Tinco Andringa* + Previously any socket errors were ignored and this made it hard to diagnose socket issues (e.g. as discussed in #28362). -* Add ActiveSupport::Notifications hook to Broadcaster#broadcast. + *Edward Poot* - *Matthew Wear* -* Close hijacked socket when connection is shut down. - - Fixes #25613. - - *Tinco Andringa* - - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index 27a17cf41b..1a0e653b69 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2016 Basecamp, LLC +Copyright (c) 2015-2017 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 28e2602cbf..d14f20d75b 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -51,10 +51,10 @@ module ApplicationCable self.current_user = find_verified_user end - protected + private def find_verified_user - if current_user = User.find_by(id: cookies.signed[:user_id]) - current_user + if verified_user = User.find_by(id: cookies.signed[:user_id]) + verified_user else reject_unauthorized_connection end @@ -167,7 +167,7 @@ App.cable.subscriptions.create "AppearanceChannel", buttonSelector = "[data-behavior~=appear_away]" install: -> - $(document).on "page:change.appearance", => + $(document).on "turbolinks:load.appearance", => @appear() $(document).on "click.appearance", buttonSelector, => @@ -326,7 +326,10 @@ Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml" ### Allowed Request Origins -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. +Action Cable will only accept requests from specific origins. + +By default, only an origin matching the cable server itself will be permitted. +Additional origins can be specified using strings or regular expressions, provided in an array. ```ruby Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/] @@ -334,12 +337,19 @@ Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonr When running in the development environment, this defaults to "http://localhost:3000". -To disable and allow requests from any origin: +To disable protection and allow requests from any origin: ```ruby Rails.application.config.action_cable.disable_request_forgery_protection = true ``` +To disable automatic access for same-origin requests, and strictly allow +only the configured origins: + +```ruby +Rails.application.config.action_cable.allow_same_origin_as_host = false +``` + ### Consumer Configuration Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup. @@ -399,7 +409,7 @@ application. The recommended basic setup is as follows: ```ruby # cable/config.ru -require ::File.expand_path('../../config/environment', __FILE__) +require ::File.expand_path('../config/environment', __dir__) Rails.application.eager_load! run ActionCable.server @@ -526,6 +536,15 @@ cable.subscriptions.create 'AppearanceChannel', # normal channel code goes here... ``` +## Download and Installation + +The latest version of Action Cable can be installed with [RubyGems](#gem-usage), +or with [npm](#npm-usage). + +Source code can be downloaded as part of the Rails project on GitHub + +* https://github.com/rails/rails/tree/master/actioncable + ## License Action Cable is released under the MIT license: diff --git a/actioncable/Rakefile b/actioncable/Rakefile index 648de57004..e21843bb44 100644 --- a/actioncable/Rakefile +++ b/actioncable/Rakefile @@ -1,17 +1,15 @@ require "rake/testtask" require "pathname" +require "open3" require "action_cable" -require "blade" - -dir = File.dirname(__FILE__) task default: :test -task package: "assets:compile" +task package: %w( assets:compile assets:verify ) Rake::TestTask.new do |t| t.libs << "test" - t.test_files = Dir.glob("#{dir}/test/**/*_test.rb") + t.test_files = Dir.glob("#{__dir__}/test/**/*_test.rb") t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -25,6 +23,7 @@ namespace :test do end task :integration do + require "blade" if ENV["CI"] Blade.start(interface: :ci) else @@ -36,6 +35,40 @@ end namespace :assets do desc "Compile Action Cable assets" task :compile do + require "blade" + require "sprockets" + require "sprockets/export" Blade.build end + + desc "Verify compiled Action Cable assets" + task :verify do + file = "lib/assets/compiled/action_cable.js" + pathname = Pathname.new("#{__dir__}/#{file}") + + print "[verify] #{file} exists " + if pathname.exist? + puts "[OK]" + else + $stderr.puts "[FAIL]" + fail + end + + print "[verify] #{file} is a UMD module " + if pathname.read =~ /module\.exports.*define\.amd/m + puts "[OK]" + else + $stderr.puts "[FAIL]" + fail + end + + print "[verify] #{__dir__} can be required as a module " + _, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{__dir__}');") + if status.success? + puts "[OK]" + else + $stderr.puts "[FAIL]\n#{stderr}" + fail + end + end end diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index e7f91d0e34..05ffd655e8 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -20,6 +20,6 @@ Gem::Specification.new do |s| s.add_dependency "actionpack", version - s.add_dependency "nio4r", "~> 1.2" + s.add_dependency "nio4r", "~> 2.0" s.add_dependency "websocket-driver", "~> 0.6.1" end diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee index 29ad676290..7fd68cad2f 100644 --- a/actioncable/app/assets/javascripts/action_cable/connection.coffee +++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee @@ -23,7 +23,7 @@ class ActionCable.Connection open: => if @isActive() ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}") - throw new Error("Existing connection must be closed before opening") + false else ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") @uninstallEventHandlers() if @webSocket? diff --git a/actioncable/bin/test b/actioncable/bin/test new file mode 100755 index 0000000000..a7beb14b27 --- /dev/null +++ b/actioncable/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require File.expand_path("../tools/test", COMPONENT_ROOT) diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index d353716636..c2d3550acb 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2015-2016 Basecamp, LLC +# Copyright (c) 2015-2017 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/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index 2e589a2cfa..718f630f58 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -122,16 +122,16 @@ module ActionCable end end - protected + private # action_methods are cached and there is sometimes need to refresh # them. ::clear_action_methods! allows you to do that, so next time # you run action_methods, they will be recalculated. - def clear_action_methods! + def clear_action_methods! # :doc: @action_methods = nil end # Refresh the cached action_methods when a new action_method is added. - def method_added(name) + def method_added(name) # :doc: super clear_action_methods! end @@ -144,13 +144,14 @@ module ActionCable # When a channel is streaming via pubsub, we want to delay the confirmation # transmission until pubsub subscription is confirmed. - @defer_subscription_confirmation = false + # + # The counter starts at 1 because it's awaiting a call to #subscribe_to_channel + @defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1) @reject_subscription = nil @subscription_confirmation_sent = nil delegate_connection_identifiers - subscribe_to_channel end # Extract the action name from the passed data and process it via the channel. The process will ensure @@ -169,6 +170,17 @@ module ActionCable end end + # This method is called after subscription has been added to the connection + # and confirms or rejects the subscription. + def subscribe_to_channel + run_callbacks :subscribe do + subscribed + end + + reject_subscription if subscription_rejected? + ensure_confirmation_sent + end + # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks. # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback. def unsubscribe_from_channel # :nodoc: @@ -177,23 +189,23 @@ module ActionCable end end - protected + private # Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams # you want this channel to be sending to the subscriber. - def subscribed + def subscribed # :doc: # Override in subclasses end # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking # users as offline or the like. - def unsubscribed + def unsubscribed # :doc: # Override in subclasses end # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with # the proper channel identifier marked as the recipient. - def transmit(data, via: nil) - logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via } + def transmit(data, via: nil) # :doc: + logger.debug "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via } payload = { channel_class: self.class.name, data: data, via: via } ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do @@ -201,27 +213,32 @@ module ActionCable end end - def defer_subscription_confirmation! - @defer_subscription_confirmation = true + def ensure_confirmation_sent # :doc: + return if subscription_rejected? + @defer_subscription_confirmation_counter.decrement + transmit_subscription_confirmation unless defer_subscription_confirmation? + end + + def defer_subscription_confirmation! # :doc: + @defer_subscription_confirmation_counter.increment end - def defer_subscription_confirmation? - @defer_subscription_confirmation + def defer_subscription_confirmation? # :doc: + @defer_subscription_confirmation_counter.value > 0 end - def subscription_confirmation_sent? + def subscription_confirmation_sent? # :doc: @subscription_confirmation_sent end - def reject + def reject # :doc: @reject_subscription = true end - def subscription_rejected? + def subscription_rejected? # :doc: @reject_subscription end - private def delegate_connection_identifiers connection.identifiers.each do |identifier| define_singleton_method(identifier) do @@ -230,18 +247,6 @@ module ActionCable end end - def subscribe_to_channel - run_callbacks :subscribe do - subscribed - end - - if subscription_rejected? - reject_subscription - else - transmit_subscription_confirmation unless defer_subscription_confirmation? - end - end - def extract_action(data) (data["action"].presence || :receive).to_sym end diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb index b7e88bf73d..b565cb3cac 100644 --- a/actioncable/lib/action_cable/channel/naming.rb +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -12,7 +12,7 @@ module ActionCable # Chats::AppearancesChannel.channel_name # => 'chats:appearances' # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances' def channel_name - @channel_name ||= name.sub(/Channel$/, "").gsub("::",":").underscore + @channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore end end diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index c9daa0bcd3..90c68cfe84 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -4,8 +4,7 @@ module ActionCable extend ActiveSupport::Concern included do - class_attribute :periodic_timers, instance_reader: false - self.periodic_timers = [] + class_attribute :periodic_timers, instance_reader: false, default: [] after_subscribe :start_periodic_timers after_unsubscribe :stop_periodic_timers diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb index 13deb62662..dbba333353 100644 --- a/actioncable/lib/action_cable/channel/streams.rb +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -84,7 +84,7 @@ module ActionCable connection.server.event_loop.post do pubsub.subscribe(broadcasting, handler, lambda do - transmit_subscription_confirmation + ensure_confirmation_sent logger.info "#{self.class.name} is streaming from #{broadcasting}" end) end diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb index 5f813cf8e0..902efb07e2 100644 --- a/actioncable/lib/action_cable/connection.rb +++ b/actioncable/lib/action_cable/connection.rb @@ -8,8 +8,6 @@ module ActionCable autoload :ClientSocket autoload :Identification autoload :InternalChannel - autoload :FayeClientSocket - autoload :FayeEventLoop autoload :MessageBuffer autoload :Stream autoload :StreamEventLoop diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb index 85df206445..989a67d6df 100644 --- a/actioncable/lib/action_cable/connection/authorization.rb +++ b/actioncable/lib/action_cable/connection/authorization.rb @@ -3,11 +3,11 @@ module ActionCable module Authorization class UnauthorizedError < StandardError; end - private - def reject_unauthorized_connection - logger.error "An unauthorized connection attempt was rejected" - raise UnauthorizedError - end + # Closes the \WebSocket connection if it is open and returns a 404 "File not Found" response. + def reject_unauthorized_connection + logger.error "An unauthorized connection attempt was rejected" + raise UnauthorizedError + end end end end diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index 4c7fcc1434..ac5f405dea 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -22,13 +22,10 @@ module ActionCable # # Any cleanup work needed when the cable connection is cut. # end # - # protected + # private # def find_verified_user - # if current_user = User.find_by_identity cookies.signed[:identity_id] - # current_user - # else + # User.find_by_identity(cookies.signed[:identity_id]) || # reject_unauthorized_connection - # end # end # end # end @@ -57,7 +54,7 @@ module ActionCable @worker_pool = server.worker_pool @logger = new_tagged_logger - @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class) + @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop) @subscriptions = ActionCable::Connection::Subscriptions.new(self) @message_buffer = ActionCable::Connection::MessageBuffer.new(self) @@ -129,16 +126,23 @@ module ActionCable end def on_error(message) # :nodoc: - # ignore + # log errors to make diagnosing socket errors easier + logger.error "WebSocket error occurred: #{message}" end def on_close(reason, code) # :nodoc: send_async :handle_close end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected + attr_reader :websocket + attr_reader :message_buffer + + private # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc. - def request + def request # :doc: @request ||= begin environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application ActionDispatch::Request.new(environment || env) @@ -146,14 +150,10 @@ module ActionCable end # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks. - def cookies + def cookies # :doc: request.cookie_jar end - attr_reader :websocket - attr_reader :message_buffer - - private def encode(cable_message) @coder.encode cable_message end @@ -195,7 +195,10 @@ module ActionCable def allow_request_origin? return true if server.config.disable_request_forgery_protection - if Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env["HTTP_ORIGIN"] } + proto = Rack::Request.new(env).ssl? ? "https" : "http" + if server.config.allow_same_origin_as_host && env["HTTP_ORIGIN"] == "#{proto}://#{env['HTTP_HOST']}" + true + elsif Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env["HTTP_ORIGIN"] } true else logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}") diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb index 70a2bbecb1..c7e30e78c8 100644 --- a/actioncable/lib/action_cable/connection/client_socket.rb +++ b/actioncable/lib/action_cable/connection/client_socket.rb @@ -90,8 +90,8 @@ module ActionCable reason ||= "" unless code == 1000 || (code >= 3000 && code <= 4999) - raise ArgumentError, "Failed to execute 'close' on WebSocket: " + - "The code must be either 1000, or between 3000 and 4999. " + + raise ArgumentError, "Failed to execute 'close' on WebSocket: " \ + "The code must be either 1000, or between 3000 and 4999. " \ "#{code} is neither." end diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb deleted file mode 100644 index 06e92c5d52..0000000000 --- a/actioncable/lib/action_cable/connection/faye_client_socket.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "faye/websocket" - -module ActionCable - module Connection - class FayeClientSocket - def initialize(env, event_target, stream_event_loop, protocols) - @env = env - @event_target = event_target - @protocols = protocols - - @faye = nil - end - - def alive? - @faye && @faye.ready_state == Faye::WebSocket::API::OPEN - end - - def transmit(data) - connect - @faye.send data - end - - def close - @faye && @faye.close - end - - def protocol - @faye && @faye.protocol - end - - def rack_response - connect - @faye.rack_response - end - - private - def connect - return if @faye - @faye = Faye::WebSocket.new(@env, @protocols) - - @faye.on(:open) { |event| @event_target.on_open } - @faye.on(:message) { |event| @event_target.on_message(event.data) } - @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) } - @faye.on(:error) { |event| @event_target.on_error(event.message) } - end - end - end -end diff --git a/actioncable/lib/action_cable/connection/faye_event_loop.rb b/actioncable/lib/action_cable/connection/faye_event_loop.rb deleted file mode 100644 index cfbe26ee6a..0000000000 --- a/actioncable/lib/action_cable/connection/faye_event_loop.rb +++ /dev/null @@ -1,44 +0,0 @@ -require "thread" - -require "eventmachine" -EventMachine.epoll if EventMachine.epoll? -EventMachine.kqueue if EventMachine.kqueue? - -module ActionCable - module Connection - class FayeEventLoop - @@mutex = Mutex.new - - def timer(interval, &block) - ensure_reactor_running - EMTimer.new(::EM::PeriodicTimer.new(interval, &block)) - end - - def post(task = nil, &block) - task ||= block - - ensure_reactor_running - ::EM.next_tick(&task) - end - - private - def ensure_reactor_running - return if EventMachine.reactor_running? - @@mutex.synchronize do - Thread.new { EventMachine.run } unless EventMachine.reactor_running? - Thread.pass until EventMachine.reactor_running? - end - end - - class EMTimer - def initialize(inner) - @inner = inner - end - - def shutdown - @inner.cancel - end - end - end - end -end diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index c91a1d1fd7..ffab359429 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -6,8 +6,7 @@ module ActionCable extend ActiveSupport::Concern included do - class_attribute :identifiers - self.identifiers = Set.new + class_attribute :identifiers, default: Set.new end class_methods do diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb index 6a80770cae..4ccd322644 100644 --- a/actioncable/lib/action_cable/connection/message_buffer.rb +++ b/actioncable/lib/action_cable/connection/message_buffer.rb @@ -28,6 +28,8 @@ module ActionCable receive_buffered_messages end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :connection attr_reader :buffered_messages diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb index 5a2aace0ba..e620b93845 100644 --- a/actioncable/lib/action_cable/connection/stream.rb +++ b/actioncable/lib/action_cable/connection/stream.rb @@ -14,6 +14,9 @@ module ActionCable @rack_hijack_io = nil @write_lock = Mutex.new + + @write_head = nil + @write_buffer = Queue.new end def each(&callback) @@ -30,14 +33,62 @@ module ActionCable end def write(data) - @write_lock.synchronize do - return @rack_hijack_io.write(data) if @rack_hijack_io - return @stream_send.call(data) if @stream_send + if @stream_send + return @stream_send.call(data) end + + if @write_lock.try_lock + begin + if @write_head.nil? && @write_buffer.empty? + written = @rack_hijack_io.write_nonblock(data, exception: false) + + case written + when :wait_writable + # proceed below + when data.bytesize + return data.bytesize + else + @write_head = data.byteslice(written, data.bytesize) + @event_loop.writes_pending @rack_hijack_io + + return data.bytesize + end + end + ensure + @write_lock.unlock + end + end + + @write_buffer << data + @event_loop.writes_pending @rack_hijack_io + + data.bytesize rescue EOFError, Errno::ECONNRESET @socket_object.client_gone end + def flush_write_buffer + @write_lock.synchronize do + loop do + if @write_head.nil? + return true if @write_buffer.empty? + @write_head = @write_buffer.pop + end + + written = @rack_hijack_io.write_nonblock(@write_head, exception: false) + case written + when :wait_writable + return false + when @write_head.bytesize + @write_head = nil + else + @write_head = @write_head.byteslice(written, @write_head.bytesize) + return false + end + end + end + end + def receive(data) @socket_object.parse(data) end @@ -55,7 +106,6 @@ module ActionCable def clean_rack_hijack return unless @rack_hijack_io @event_loop.detach(@rack_hijack_io, self) - @rack_hijack_io.close @rack_hijack_io = nil end end diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb index 106b948c45..2d1af0ff9f 100644 --- a/actioncable/lib/action_cable/connection/stream_event_loop.rb +++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb @@ -5,7 +5,7 @@ module ActionCable module Connection class StreamEventLoop def initialize - @nio = @thread = nil + @nio = @executor = @thread = nil @map = {} @stopping = false @todo = Queue.new @@ -20,13 +20,14 @@ module ActionCable def post(task = nil, &block) task ||= block - Concurrent.global_io_executor << task + spawn + @executor << task end def attach(io, stream) @todo << lambda do - @map[io] = stream - @nio.register(io, :r) + @map[io] = @nio.register(io, :r) + @map[io].value = stream end wakeup end @@ -35,6 +36,16 @@ module ActionCable @todo << lambda do @nio.deregister io @map.delete io + io.close + end + wakeup + end + + def writes_pending(io) + @todo << lambda do + if monitor = @map[io] + monitor.interests = :rw + end end wakeup end @@ -52,6 +63,13 @@ module ActionCable return if @thread && @thread.status @nio ||= NIO::Selector.new + + @executor ||= Concurrent::ThreadPoolExecutor.new( + min_threads: 1, + max_threads: 10, + max_queue: 0, + ) + @thread = Thread.new { run } return true @@ -77,12 +95,25 @@ module ActionCable monitors.each do |monitor| io = monitor.io - stream = @map[io] + stream = monitor.value begin - stream.receive io.read_nonblock(4096) - rescue IO::WaitReadable - next + if monitor.writable? + if stream.flush_write_buffer + monitor.interests = :r + end + next unless monitor.readable? + end + + incoming = io.read_nonblock(4096, exception: false) + case incoming + when :wait_readable + next + when nil + stream.close + else + stream.receive incoming + end rescue # We expect one of EOFError or Errno::ECONNRESET in # normal operation (when the client goes away). But if diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index 9060183249..44bce1e195 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -19,17 +19,21 @@ module ActionCable logger.error "Received unrecognized command in #{data.inspect}" end rescue Exception => e - logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}" + logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}" end def add(data) id_key = data["identifier"] id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access + return if subscriptions.key?(id_key) + subscription_klass = id_options[:channel].safe_constantize if subscription_klass && ActionCable::Channel::Base >= subscription_klass - subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options) + subscription = subscription_klass.new(connection, id_key, id_options) + subscriptions[id_key] = subscription + subscription.subscribe_to_channel else logger.error "Subscription class not found: #{id_options[:channel].inspect}" end @@ -57,6 +61,8 @@ module ActionCable subscriptions.each { |id, channel| remove_subscription(channel) } end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :connection, :subscriptions diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb index 41afa9680a..aef549aa86 100644 --- a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb +++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb @@ -31,8 +31,8 @@ module ActionCable end end - protected - def log(type, message) + private + def log(type, message) # :doc: tag(@logger) { @logger.send type, message } end end diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb index 52d8daad4b..03eb6e2ea8 100644 --- a/actioncable/lib/action_cable/connection/web_socket.rb +++ b/actioncable/lib/action_cable/connection/web_socket.rb @@ -4,8 +4,8 @@ module ActionCable module Connection # Wrap the real socket to minimize the externally-presented API class WebSocket - def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols]) - @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil + def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols]) + @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil end def possible? @@ -32,6 +32,8 @@ module ActionCable websocket.rack_response end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :websocket end diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index 4c5c975cd8..63a26636a0 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -31,10 +31,10 @@ module ActionCable self.cable = Rails.application.config_for(config_path).with_indifferent_access end - previous_connection_class = self.connection_class + previous_connection_class = connection_class self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call } - options.each { |k,v| send("#{k}=", v) } + options.each { |k, v| send("#{k}=", v) } end end diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index 8ba0230d47..5d6f9af0bb 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -6,7 +6,7 @@ module ActionCable module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb index 2081a37db6..f53be0bc31 100644 --- a/actioncable/lib/action_cable/helpers/action_cable_helper.rb +++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb @@ -6,7 +6,7 @@ module ActionCable # # <head> # <%= action_cable_meta_tag %> - # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %> # </head> # # This is then used by Action Cable to determine the URL of your WebSocket server. diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb index 720ba52d19..e689fbf21b 100644 --- a/actioncable/lib/action_cable/remote_connections.rb +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -45,7 +45,7 @@ module ActionCable end # Returns all the identifiers that were applied to this connection. - def identifiers + redefine_method :identifiers do server.connection_identifiers end @@ -54,7 +54,7 @@ module ActionCable def set_identifier_instance_vars(ids) raise InvalidIdentifiersError unless valid_identifiers?(ids) - ids.each { |k,v| instance_variable_set("@#{k}", v) } + ids.each { |k, v| instance_variable_set("@#{k}", v) } end def valid_identifiers?(ids) diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index dd059a553b..419eccd73c 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -37,9 +37,13 @@ module ActionCable connections.each(&:close) @mutex.synchronize do - worker_pool.halt if @worker_pool - + # Shutdown the worker pool + @worker_pool.halt if @worker_pool @worker_pool = nil + + # Shutdown the pub/sub adapter + @pubsub.shutdown if @pubsub + @pubsub = nil end end @@ -49,7 +53,7 @@ module ActionCable end def event_loop - @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new } + @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new } end # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread. diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index 1fc58baa3e..7fcd6c6587 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -38,7 +38,7 @@ module ActionCable end def broadcast(message) - server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" + server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" payload = { broadcasting: broadcasting, message: message, coder: coder } ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb index 7153593d4c..17e0dee064 100644 --- a/actioncable/lib/action_cable/server/configuration.rb +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -4,8 +4,8 @@ module ActionCable # in a Rails config initializer. class Configuration attr_accessor :logger, :log_tags - attr_accessor :use_faye, :connection_class, :worker_pool_size - attr_accessor :disable_request_forgery_protection, :allowed_request_origins + attr_accessor :connection_class, :worker_pool_size + attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host attr_accessor :cable, :url, :mount_path def initialize @@ -15,6 +15,7 @@ module ActionCable @worker_pool_size = 4 @disable_request_forgery_protection = false + @allow_same_origin_as_host = true end # Returns constant of subscription adapter specified in config/cable.yml. @@ -35,22 +36,6 @@ module ActionCable adapter = "PostgreSQL" if adapter == "Postgresql" "ActionCable::SubscriptionAdapter::#{adapter}".constantize end - - def event_loop_class - if use_faye - ActionCable::Connection::FayeEventLoop - else - ActionCable::Connection::StreamEventLoop - end - end - - def client_socket_class - if use_faye - ActionCable::Connection::FayeClientSocket - else - ActionCable::Connection::ClientSocket - end - end end end end diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index 7460472551..43639c27af 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -25,7 +25,7 @@ module ActionCable # Stop processing work: any work that has not already started # running will be discarded from the queue def halt - @executor.kill + @executor.shutdown end def stopping? diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb index 72e62f3daf..596269ab9b 100644 --- a/actioncable/lib/action_cable/subscription_adapter.rb +++ b/actioncable/lib/action_cable/subscription_adapter.rb @@ -4,5 +4,6 @@ module ActionCable autoload :Base autoload :SubscriberMap + autoload :ChannelPrefix end end diff --git a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb new file mode 100644 index 0000000000..8b293cc785 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb @@ -0,0 +1,26 @@ +module ActionCable + module SubscriptionAdapter + module ChannelPrefix # :nodoc: + def broadcast(channel, payload) + channel = channel_with_prefix(channel) + super + end + + def subscribe(channel, callback, success_callback = nil) + channel = channel_with_prefix(channel) + super + end + + def unsubscribe(channel, callback) + channel = channel_with_prefix(channel) + super + end + + private + # Returns the channel name, including channel_prefix specified in cable.yml + def channel_with_prefix(channel) + [@server.config.cable[:channel_prefix], channel].compact.join(":") + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb index bcd46d2a0e..ed8f315791 100644 --- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb @@ -11,6 +11,8 @@ EventMachine.kqueue if EventMachine.kqueue? module ActionCable module SubscriptionAdapter class EventedRedis < Base # :nodoc: + prepend ChannelPrefix + @@mutex = Mutex.new # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis. @@ -22,6 +24,12 @@ module ActionCable cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } def initialize(*) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The "evented_redis" subscription adapter is deprecated and + will be removed in Rails 5.2. Please use the "redis" adapter + instead. + MSG + super @redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil end @@ -68,10 +76,10 @@ module ActionCable end def ensure_reactor_running - return if EventMachine.reactor_running? + return if EventMachine.reactor_running? && EventMachine.reactor_thread @@mutex.synchronize do Thread.new { EventMachine.run } unless EventMachine.reactor_running? - Thread.pass until EventMachine.reactor_running? + Thread.pass until EventMachine.reactor_running? && EventMachine.reactor_thread end end end diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index 62bd284a6b..41a6e55822 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -6,6 +6,8 @@ require "redis" module ActionCable module SubscriptionAdapter class Redis < Base # :nodoc: + prepend ChannelPrefix + # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis. # This is needed, for example, when using Makara proxies for distributed Redis. cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } diff --git a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb index 4ec513e3ba..4cce86dcca 100644 --- a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb +++ b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb @@ -2,7 +2,7 @@ module ActionCable module SubscriptionAdapter class SubscriberMap def initialize - @subscribers = Hash.new { |h,k| h[k] = [] } + @subscribers = Hash.new { |h, k| h[k] = [] } @sync = Mutex.new end diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE index 6249553c22..dd109fda80 100644 --- a/actioncable/lib/rails/generators/channel/USAGE +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -3,7 +3,7 @@ Description: Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript). Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments. - Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels. + Note: Turn on the cable connection in app/assets/javascripts/cable.js after generating any channels. Example: ======== @@ -11,4 +11,4 @@ Example: creates a Chat channel class and CoffeeScript asset: Channel: app/channels/chat_channel.rb - Assets: app/assets/javascript/channels/chat.coffee + Assets: app/assets/javascripts/channels/chat.coffee diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 20d807c033..80f512c94c 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -1,7 +1,7 @@ module Rails module Generators class ChannelGenerator < NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) argument :actions, type: :array, default: [], banner: "method method" @@ -13,7 +13,7 @@ module Rails template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb") if options[:assets] - if self.behavior == :invoke + if behavior == :invoke template "assets/cable.js", "app/assets/javascripts/cable.js" end @@ -23,14 +23,14 @@ module Rails generate_application_cable_files end - protected + private def file_name @_file_name ||= super.gsub(/_channel/i, "") end # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required. def generate_application_cable_files - return if self.behavior != :invoke + return if behavior != :invoke files = [ "application_cable/channel.rb", diff --git a/actioncable/package.json b/actioncable/package.json index 37f82fa1ea..acec1e2e9c 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -1,6 +1,6 @@ { "name": "actioncable", - "version": "5.0.0-rc1", + "version": "5.2.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 2bb3214f74..9a3a3581e6 100644 --- a/actioncable/test/channel/base_test.rb +++ b/actioncable/test/channel/base_test.rb @@ -77,11 +77,13 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase @channel = ChatChannel.new @connection, "{id: 1}", id: 1 end - test "should subscribe to a channel on initialize" do + test "should subscribe to a channel" do + @channel.subscribe_to_channel assert_equal 1, @channel.room.id end test "on subscribe callbacks" do + @channel.subscribe_to_channel assert @channel.subscribed end @@ -90,6 +92,8 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase end test "unsubscribing from a channel" do + @channel.subscribe_to_channel + assert @channel.room assert @channel.subscribed? @@ -150,8 +154,13 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase assert_equal expected, @connection.last_transmission end - test "subscription confirmation" do + test "do not send subscription confirmation on initialize" do + assert_nil @connection.last_transmission + end + + test "subscription confirmation on subscribe_to_channel" do expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + @channel.subscribe_to_channel assert_equal expected, @connection.last_transmission end @@ -208,6 +217,8 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase test "notification for transmit_subscription_confirmation" do begin + @channel.subscribe_to_channel + events = [] ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args| events << ActiveSupport::Notifications::Event.new(*args) diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb index 529abb9535..17a8e45a35 100644 --- a/actioncable/test/channel/periodic_timers_test.rb +++ b/actioncable/test/channel/periodic_timers_test.rb @@ -32,29 +32,32 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase timers.each_with_index do |timer, i| assert_kind_of Proc, timer[0] - assert_equal i+1, timer[1][:every] + assert_equal i + 1, timer[1][:every] end end test "disallow negative and zero periods" do [ 0, 0.0, 0.seconds, -1, -1.seconds, "foo", :foo, Object.new ].each do |invalid| - assert_raise ArgumentError, /Expected every:/ do + e = assert_raise ArgumentError do ChatChannel.periodically :send_updates, every: invalid end + assert_match(/Expected every:/, e.message) end end test "disallow block and arg together" do - assert_raise ArgumentError, /not both/ do + e = assert_raise ArgumentError do ChatChannel.periodically(:send_updates, every: 1) { ping } end + assert_match(/not both/, e.message) end test "disallow unknown args" do [ "send_updates", Object.new, nil ].each do |invalid| - assert_raise ArgumentError, /Expected a Symbol/ do + e = assert_raise ArgumentError do ChatChannel.periodically invalid, every: 1 end + assert_match(/Expected a Symbol/, e.message) end end @@ -62,6 +65,7 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase @connection.server.event_loop.expects(:timer).times(3).returns(stub(shutdown: nil)) channel = ChatChannel.new @connection, "{id: 1}", id: 1 + channel.subscribe_to_channel channel.unsubscribe_from_channel assert_equal [], channel.send(:active_periodic_timers) end diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb index faf35ad048..99c4a7603a 100644 --- a/actioncable/test/channel/rejection_test.rb +++ b/actioncable/test/channel/rejection_test.rb @@ -20,6 +20,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase test "subscription rejection" do @connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) } @channel = SecretChannel.new @connection, "{id: 1}", id: 1 + @channel.subscribe_to_channel expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } assert_equal expected, @connection.last_transmission @@ -28,6 +29,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase test "does not execute action if subscription is rejected" do @connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) } @channel = SecretChannel.new @connection, "{id: 1}", id: 1 + @channel.subscribe_to_channel expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } assert_equal expected, @connection.last_transmission diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index da26f81a5c..50fc7be30b 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -53,6 +53,9 @@ module ActionCable::StreamTests connection = TestConnection.new connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } channel = ChatChannel.new connection, "{id: 1}", id: 1 + channel.subscribe_to_channel + + wait_for_async connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } channel.unsubscribe_from_channel @@ -64,6 +67,9 @@ module ActionCable::StreamTests connection = TestConnection.new connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } channel = SymbolChannel.new connection, "" + channel.subscribe_to_channel + + wait_for_async connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } channel.unsubscribe_from_channel @@ -76,6 +82,7 @@ module ActionCable::StreamTests connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:stream_tests:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } channel = ChatChannel.new connection, "" + channel.subscribe_to_channel channel.stream_for Room.new(1) end end @@ -84,7 +91,9 @@ module ActionCable::StreamTests run_in_eventmachine do connection = TestConnection.new - ChatChannel.new connection, "{id: 1}", id: 1 + channel = ChatChannel.new connection, "{id: 1}", id: 1 + channel.subscribe_to_channel + assert_nil connection.last_transmission wait_for_async @@ -114,7 +123,7 @@ module ActionCable::StreamTests end end - require "action_cable/subscription_adapter/inline" + require "action_cable/subscription_adapter/async" class UserCallbackChannel < ActionCable::Channel::Base def subscribed @@ -124,9 +133,16 @@ module ActionCable::StreamTests end end - class StreamEncodingTest < ActionCable::TestCase + class MultiChatChannel < ActionCable::Channel::Base + def subscribed + stream_from "main_room" + stream_from "test_all_rooms" + end + end + + class StreamFromTest < ActionCable::TestCase setup do - @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Inline) + @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async) @server.config.allowed_request_origins = %w( http://rubyonrails.com ) end @@ -153,6 +169,17 @@ module ActionCable::StreamTests end end + test "subscription confirmation should only be sent out once with muptiple stream_from" do + run_in_eventmachine do + connection = open_connection + expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" } + connection.websocket.expects(:transmit).with(expected.to_json) + receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {}) + + wait_for_async + end + end + private def subscribe_to(connection, identifiers:) receive connection, command: "subscribe", identifiers: identifiers diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index 2e3821828f..30ac1e9c38 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -1,13 +1,31 @@ require "test_helper" require "concurrent" -require "faye/websocket" +require "websocket-client-simple" require "json" require "active_support/hash_with_indifferent_access" +#### +# 😷 Warning suppression 😷 +WebSocket::Frame::Handler::Handler03.prepend Module.new { + def initialize(*) + @application_data_buffer = nil + super + end +} + +WebSocket::Frame::Data.prepend Module.new { + def initialize(*) + @masking_key = nil + super + end +} +# +#### + class ClientTest < ActionCable::TestCase - WAIT_WHEN_EXPECTING_EVENT = 8 + WAIT_WHEN_EXPECTING_EVENT = 2 WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5 class EchoChannel < ActionCable::Channel::Base @@ -39,20 +57,9 @@ class ClientTest < ActionCable::TestCase server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async") - server.config.use_faye = ENV["FAYE"].present? # and now the "real" setup for our test: server.config.disable_request_forgery_protection = true - - Thread.new { EventMachine.run } unless EventMachine.reactor_running? - Thread.pass until EventMachine.reactor_running? - - # faye-websocket is warning-rich - @previous_verbose, $VERBOSE = $VERBOSE, nil - end - - def teardown - $VERBOSE = @previous_verbose end def with_puma_server(rack_app = ActionCable.server, port = 3099) @@ -61,56 +68,82 @@ class ClientTest < ActionCable::TestCase server.min_threads = 1 server.max_threads = 4 - t = Thread.new { server.run.join } - yield port + thread = server.run + + begin + yield port + + ensure + server.stop + + begin + thread.join + + rescue IOError + # Work around https://bugs.ruby-lang.org/issues/13405 + # + # Puma's sometimes raising while shutting down, when it closes + # its internal pipe. We can safely ignore that, but we do need + # to do the step skipped by the exception: + server.binder.close + + rescue RuntimeError => ex + # Work around https://bugs.ruby-lang.org/issues/13239 + raise unless ex.message =~ /can't modify frozen IOError/ - ensure - server.stop(true) if server - t.join if t + # Handle this as if it were the IOError: do the same as above. + server.binder.close + end + end end class SyncClient attr_reader :pings def initialize(port) - @ws = Faye::WebSocket::Client.new("ws://127.0.0.1:#{port}/") - @messages = Queue.new - @closed = Concurrent::Event.new - @has_messages = Concurrent::Semaphore.new(0) - @pings = 0 - - open = Concurrent::Event.new - error = nil - - @ws.on(:error) do |event| - if open.set? - @messages << RuntimeError.new(event.message) - else - error = event.message - open.set + messages = @messages = Queue.new + closed = @closed = Concurrent::Event.new + has_messages = @has_messages = Concurrent::Semaphore.new(0) + pings = @pings = Concurrent::AtomicFixnum.new(0) + + open = Concurrent::Promise.new + + @ws = WebSocket::Client::Simple.connect("ws://127.0.0.1:#{port}/") do |ws| + ws.on(:error) do |event| + event = RuntimeError.new(event.message) unless event.is_a?(Exception) + + if open.pending? + open.fail(event) + else + messages << event + has_messages.release + end end - end - @ws.on(:open) do |event| - open.set - end + ws.on(:open) do |event| + open.set(true) + end - @ws.on(:message) do |event| - message = JSON.parse(event.data) - if message["type"] == "ping" - @pings += 1 - else - @messages << message - @has_messages.release + ws.on(:message) do |event| + if event.type == :close + closed.set + else + message = JSON.parse(event.data) + if message["type"] == "ping" + pings.increment + else + messages << message + has_messages.release + end + end end - end - @ws.on(:close) do |event| - @closed.set + ws.on(:close) do |event| + closed.set + end end - open.wait(WAIT_WHEN_EXPECTING_EVENT) - raise error if error + open.wait!(WAIT_WHEN_EXPECTING_EVENT) end def read_message @@ -161,76 +194,80 @@ class ClientTest < ActionCable::TestCase end end - def faye_client(port) + def websocket_client(port) SyncClient.new(port) end + def concurrently(enum) + enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!) + end + def test_single_client with_puma_server do |port| - c = faye_client(port) + c = websocket_client(port) assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel") - assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message) c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello") - assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "message"=>{ "dong"=>"hello" } }, c.read_message) + assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "message" => { "dong" => "hello" } }, c.read_message) c.close end end def test_interacting_clients with_puma_server do |port| - clients = 10.times.map { faye_client(port) } + clients = concurrently(10.times) { websocket_client(port) } barrier_1 = Concurrent::CyclicBarrier.new(clients.size) barrier_2 = Concurrent::CyclicBarrier.new(clients.size) - clients.map { |c| Concurrent::Future.execute { + concurrently(clients) do |c| assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel") - assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "type" => "confirm_subscription" }, c.read_message) c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello") - assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{ "dong"=>"hello" } }, c.read_message) + assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message) barrier_1.wait WAIT_WHEN_EXPECTING_EVENT c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "bulk", message: "hello") barrier_2.wait WAIT_WHEN_EXPECTING_EVENT assert_equal clients.size, c.read_messages(clients.size).size - } }.each(&:wait!) + end - clients.map { |c| Concurrent::Future.execute { c.close } }.each(&:wait!) + concurrently(clients, &:close) end end def test_many_clients with_puma_server do |port| - clients = 100.times.map { faye_client(port) } + clients = concurrently(100.times) { websocket_client(port) } - clients.map { |c| Concurrent::Future.execute { + concurrently(clients) do |c| assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel") - assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "type" => "confirm_subscription" }, c.read_message) c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello") - assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{ "dong"=>"hello" } }, c.read_message) - } }.each(&:wait!) + assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message) + end - clients.map { |c| Concurrent::Future.execute { c.close } }.each(&:wait!) + concurrently(clients, &:close) end end def test_disappearing_client with_puma_server do |port| - c = faye_client(port) + c = websocket_client(port) assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel") - assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message) c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "delay", message: "hello") c.close # disappear before write - c = faye_client(port) + c = websocket_client(port) assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel") - assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message) c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello") - assert_equal({ "identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{ "dong"=>"hello" } }, c.read_message) + assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message) c.close # disappear before read end end @@ -240,10 +277,10 @@ class ClientTest < ActionCable::TestCase app = ActionCable.server identifier = JSON.generate(channel: "ClientTest::EchoChannel") - c = faye_client(port) + c = websocket_client(port) assert_equal({ "type" => "welcome" }, c.read_message) c.send_message command: "subscribe", identifier: identifier - assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message) assert_equal(1, app.connections.count) assert(app.remote_connections.where(identifier: identifier)) @@ -261,10 +298,10 @@ class ClientTest < ActionCable::TestCase def test_server_restart with_puma_server do |port| - c = faye_client(port) + c = websocket_client(port) assert_equal({ "type" => "welcome" }, c.read_message) c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel") - assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message) + assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message) ActionCable.server.restart c.wait_for_close diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb index 5043a63370..bc3ff6a3d7 100644 --- a/actioncable/test/connection/client_socket_test.rb +++ b/actioncable/test/connection/client_socket_test.rb @@ -33,8 +33,6 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase end test "delegate socket errors to on_error handler" do - skip if ENV["FAYE"].present? - run_in_eventmachine do connection = open_connection @@ -49,16 +47,16 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase end test "closes hijacked i/o socket at shutdown" do - skip if ENV["FAYE"].present? - run_in_eventmachine do connection = open_connection client = connection.websocket.send(:websocket) + event = Concurrent::Event.new client.instance_variable_get("@stream") .instance_variable_get("@rack_hijack_io") - .expects(:close) + .define_singleton_method(:close) { event.set } connection.close + event.wait end end @@ -67,7 +65,13 @@ 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" - env["rack.hijack"] = -> { env["rack.hijack_io"] = StringIO.new } + io = \ + begin + Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0).first + rescue + StringIO.new + end + env["rack.hijack"] = -> { env["rack.hijack_io"] = io } Connection.new(@server, env).tap do |connection| connection.process diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb index 3bc59c9db9..37bedfd734 100644 --- a/actioncable/test/connection/cross_site_forgery_test.rb +++ b/actioncable/test/connection/cross_site_forgery_test.rb @@ -13,11 +13,13 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase setup do @server = TestServer.new @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + @server.config.allow_same_origin_as_host = false end teardown do @server.config.disable_request_forgery_protection = false @server.config.allowed_request_origins = [] + @server.config.allow_same_origin_as_host = true end test "disable forgery protection" do @@ -53,6 +55,13 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase assert_origin_not_allowed "http://rails.co.uk" end + test "allow same origin as host" do + @server.config.allow_same_origin_as_host = true + assert_origin_allowed "http://#{HOST}" + assert_origin_not_allowed "http://hax.com" + assert_origin_not_allowed "http://rails.co.uk" + end + private def assert_origin_allowed(origin) response = connect_with_origin origin diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb index a4dfcc06f0..f3d3bc0603 100644 --- a/actioncable/test/connection/identifier_test.rb +++ b/actioncable/test/connection/identifier_test.rb @@ -53,7 +53,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase end end - protected + private def open_connection_with_stubbed_pubsub server = TestServer.new server.stubs(:adapter).returns(stub_everything("adapter")) diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb index 67e68355c5..ca1a08f4d6 100644 --- a/actioncable/test/connection/multiple_identifiers_test.rb +++ b/actioncable/test/connection/multiple_identifiers_test.rb @@ -19,7 +19,7 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase end end - protected + private def open_connection_with_stubbed_pubsub server = TestServer.new server.stubs(:pubsub).returns(stub_everything("pubsub")) diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb index 4128b32f15..36e1d3c095 100644 --- a/actioncable/test/connection/stream_test.rb +++ b/actioncable/test/connection/stream_test.rb @@ -34,8 +34,6 @@ class ActionCable::Connection::StreamTest < ActionCable::TestCase [ EOFError, Errno::ECONNRESET ].each do |closed_exception| test "closes socket on #{closed_exception}" do - skip if ENV["FAYE"].present? - run_in_eventmachine do connection = open_connection diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb index 87484765e5..6d53e249cb 100644 --- a/actioncable/test/connection/string_identifier_test.rb +++ b/actioncable/test/connection/string_identifier_test.rb @@ -21,7 +21,7 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase end end - protected + private def open_connection_with_stubbed_pubsub @server = TestServer.new @server.stubs(:pubsub).returns(stub_everything("pubsub")) diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee index 6b145dede8..a9e95c37f0 100644 --- a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee +++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee @@ -21,6 +21,16 @@ TestHelpers.consumerTest = (name, options = {}, callback) -> assert.equal clients.length, 1 assert.equal clients[0].readyState, WebSocket.OPEN + server.broadcastTo = (subscription, data = {}, callback) -> + data.identifier = subscription.identifier + + if data.message_type + data.type = ActionCable.INTERNAL.message_types[data.message_type] + delete data.message_type + + server.send(JSON.stringify(data)) + TestHelpers.defer(callback) + done = -> consumer.disconnect() server.close() diff --git a/actioncable/test/javascript/src/test_helpers/index.coffee b/actioncable/test/javascript/src/test_helpers/index.coffee index d36524d9cc..c84cbbcb2c 100644 --- a/actioncable/test/javascript/src/test_helpers/index.coffee +++ b/actioncable/test/javascript/src/test_helpers/index.coffee @@ -4,5 +4,8 @@ ActionCable.TestHelpers = testURL: "ws://cable.example.com/" + defer: (callback) -> + setTimeout(callback, 1) + originalWebSocket = ActionCable.WebSocket QUnit.testDone -> ActionCable.WebSocket = originalWebSocket diff --git a/actioncable/test/javascript/src/unit/consumer_test.coffee b/actioncable/test/javascript/src/unit/consumer_test.coffee index cf8a592255..41445274eb 100644 --- a/actioncable/test/javascript/src/unit/consumer_test.coffee +++ b/actioncable/test/javascript/src/unit/consumer_test.coffee @@ -2,8 +2,11 @@ {consumerTest} = ActionCable.TestHelpers module "ActionCable.Consumer", -> - consumerTest "#connect", connect: false, ({consumer, server, done}) -> - server.on("connection", done) + consumerTest "#connect", connect: false, ({consumer, server, assert, done}) -> + server.on "connection", -> + assert.equal consumer.connect(), false + done() + consumer.connect() consumerTest "#disconnect", ({consumer, client, done}) -> diff --git a/actioncable/test/javascript/src/unit/subscription_test.coffee b/actioncable/test/javascript/src/unit/subscription_test.coffee new file mode 100644 index 0000000000..07027ed170 --- /dev/null +++ b/actioncable/test/javascript/src/unit/subscription_test.coffee @@ -0,0 +1,40 @@ +{module, test} = QUnit +{consumerTest} = ActionCable.TestHelpers + +module "ActionCable.Subscription", -> + consumerTest "#initialized callback", ({server, consumer, assert, done}) -> + consumer.subscriptions.create "chat", + initialized: -> + assert.ok true + done() + + consumerTest "#connected callback", ({server, consumer, assert, done}) -> + subscription = consumer.subscriptions.create "chat", + connected: -> + assert.ok true + done() + + server.broadcastTo(subscription, message_type: "confirmation") + + consumerTest "#disconnected callback", ({server, consumer, assert, done}) -> + subscription = consumer.subscriptions.create "chat", + disconnected: -> + assert.ok true + done() + + server.broadcastTo subscription, message_type: "confirmation", -> + server.close() + + consumerTest "#perform", ({consumer, server, assert, done}) -> + subscription = consumer.subscriptions.create "chat", + connected: -> + @perform(publish: "hi") + + server.on "message", (message) -> + data = JSON.parse(message) + assert.equal data.identifier, subscription.identifier + assert.equal data.command, "message" + assert.deepEqual data.data, JSON.stringify(action: { publish: "hi" }) + done() + + server.broadcastTo(subscription, message_type: "confirmation") diff --git a/actioncable/test/javascript/src/unit/subscriptions_test.coffee b/actioncable/test/javascript/src/unit/subscriptions_test.coffee new file mode 100644 index 0000000000..170b370e4a --- /dev/null +++ b/actioncable/test/javascript/src/unit/subscriptions_test.coffee @@ -0,0 +1,25 @@ +{module, test} = QUnit +{consumerTest} = ActionCable.TestHelpers + +module "ActionCable.Subscriptions", -> + consumerTest "create subscription with channel string", ({consumer, server, assert, done}) -> + channel = "chat" + + server.on "message", (message) -> + data = JSON.parse(message) + assert.equal data.command, "subscribe" + assert.equal data.identifier, JSON.stringify({channel}) + done() + + consumer.subscriptions.create(channel) + + consumerTest "create subscription with channel object", ({consumer, server, assert, done}) -> + channel = channel: "chat", room: "action" + + server.on "message", (message) -> + data = JSON.parse(message) + assert.equal data.command, "subscribe" + assert.equal data.identifier, JSON.stringify(channel) + done() + + consumer.subscriptions.create(channel) diff --git a/actioncable/test/server/base_test.rb b/actioncable/test/server/base_test.rb new file mode 100644 index 0000000000..f0a51c5a7d --- /dev/null +++ b/actioncable/test/server/base_test.rb @@ -0,0 +1,33 @@ +require "test_helper" +require "stubs/test_server" +require "active_support/core_ext/hash/indifferent_access" + +class BaseTest < ActiveSupport::TestCase + def setup + @server = ActionCable::Server::Base.new + @server.config.cable = { adapter: "async" }.with_indifferent_access + end + + class FakeConnection + def close + end + end + + test "#restart closes all open connections" do + conn = FakeConnection.new + @server.add_connection(conn) + + conn.expects(:close) + @server.restart + end + + test "#restart shuts down worker pool" do + @server.worker_pool.expects(:halt) + @server.restart + end + + test "#restart shuts down pub/sub adapter" do + @server.pubsub.expects(:shutdown) + @server.restart + end +end diff --git a/actioncable/test/stubs/room.rb b/actioncable/test/stubs/room.rb index 9e521cf3a6..1664b07d12 100644 --- a/actioncable/test/stubs/room.rb +++ b/actioncable/test/stubs/room.rb @@ -1,7 +1,7 @@ class Room attr_reader :id, :name - def initialize(id, name="Campfire") + def initialize(id, name = "Campfire") @id = id @name = name end diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb index b64ff33789..5bf2a151dc 100644 --- a/actioncable/test/stubs/test_server.rb +++ b/actioncable/test/stubs/test_server.rb @@ -10,12 +10,6 @@ class TestServer @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter) - @config.use_faye = ENV["FAYE"].present? - @config.client_socket_class = if @config.use_faye - ActionCable::Connection::FayeClientSocket - else - ActionCable::Connection::ClientSocket - end @mutex = Monitor.new end @@ -25,10 +19,8 @@ class TestServer end def event_loop - @event_loop ||= if @config.use_faye - ActionCable::Connection::FayeEventLoop.new - else - ActionCable::Connection::StreamEventLoop.new + @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop| + loop.instance_variable_set(:@executor, Concurrent.global_io_executor) end end diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb new file mode 100644 index 0000000000..9ad659912e --- /dev/null +++ b/actioncable/test/subscription_adapter/channel_prefix.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class ActionCable::Server::WithIndependentConfig < ActionCable::Server::Base + # ActionCable::Server::Base defines config as a class variable. + # Need config to be an instance variable here as we're testing 2 separate configs + def config + @config ||= ActionCable::Server::Configuration.new + end +end + +module ChannelPrefixTest + def test_channel_prefix + server2 = ActionCable::Server::WithIndependentConfig.new + server2.config.cable = alt_cable_config + server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + adapter_klass = server2.config.pubsub_adapter + + rx_adapter2 = adapter_klass.new(server2) + tx_adapter2 = adapter_klass.new(server2) + + subscribe_as_queue("channel") do |queue| + subscribe_as_queue("channel", rx_adapter2) do |queue2| + @tx_adapter.broadcast("channel", "hello world") + tx_adapter2.broadcast("channel", "hello world 2") + + assert_equal "hello world", queue.pop + assert_equal "hello world 2", queue2.pop + end + end + end + + def alt_cable_config + cable_config.merge(channel_prefix: "foo") + end +end diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb index 1538157995..3aa88c2caa 100644 --- a/actioncable/test/subscription_adapter/common.rb +++ b/actioncable/test/subscription_adapter/common.rb @@ -11,7 +11,7 @@ module CommonSubscriptionAdapterTest def setup server = ActionCable::Server::Base.new server.config.cable = cable_config.with_indifferent_access - server.config.use_faye = ENV["FAYE"].present? + server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } adapter_klass = server.config.pubsub_adapter @@ -20,7 +20,7 @@ module CommonSubscriptionAdapterTest end def teardown - [@rx_adapter, @tx_adapter].uniq.each(&:shutdown) + [@rx_adapter, @tx_adapter].uniq.compact.each(&:shutdown) end def subscribe_as_queue(channel, adapter = @rx_adapter) diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb index d6ca0e77cb..256458bc24 100644 --- a/actioncable/test/subscription_adapter/evented_redis_test.rb +++ b/actioncable/test/subscription_adapter/evented_redis_test.rb @@ -1,20 +1,58 @@ require "test_helper" require_relative "./common" +require_relative "./channel_prefix" class EventedRedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest + include ChannelPrefixTest def setup - super + assert_deprecated do + super + end # em-hiredis is warning-rich @previous_verbose, $VERBOSE = $VERBOSE, nil end def teardown + super + + # Ensure EM is shut down before we re-enable warnings + EventMachine.reactor_thread.tap do |thread| + EventMachine.stop + thread.join + end + $VERBOSE = @previous_verbose end + def test_slow_eventmachine + require "eventmachine" + require "thread" + + lock = Mutex.new + + EventMachine.singleton_class.class_eval do + alias_method :delayed_initialize_event_machine, :initialize_event_machine + define_method(:initialize_event_machine) do + lock.synchronize do + sleep 0.5 + delayed_initialize_event_machine + end + end + end + + test_basic_broadcast + ensure + lock.synchronize do + EventMachine.singleton_class.class_eval do + alias_method :initialize_event_machine, :delayed_initialize_event_machine + remove_method :delayed_initialize_event_machine + end + end + end + def cable_config { adapter: "evented_redis", url: "redis://127.0.0.1:6379/12" } end diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 2ba5636656..4df5e0cbcd 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -1,8 +1,10 @@ require "test_helper" require_relative "./common" +require_relative "./channel_prefix" class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest + include ChannelPrefixTest def cable_config { adapter: "redis", driver: "ruby", url: "redis://127.0.0.1:6379/12" } diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 39855ea252..5d246c2b76 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -11,43 +11,9 @@ rescue LoadError end # Require all the stubs and models -Dir[File.dirname(__FILE__) + "/stubs/*.rb"].each { |file| require file } +Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file } -if ENV["FAYE"].present? - require "faye/websocket" - class << Faye::WebSocket - remove_method :ensure_reactor_running - - # We don't want Faye to start the EM reactor in tests because it makes testing much harder. - # We want to be able to start and stop EM loop in tests to make things simpler. - def ensure_reactor_running - # no-op - end - end -end - -module EventMachineConcurrencyHelpers - def wait_for_async - EM.run_deferred_callbacks - end - - def run_in_eventmachine - failure = nil - EM.run do - begin - yield - rescue => ex - failure = ex - ensure - wait_for_async - EM.stop if EM.reactor_running? - end - end - raise failure if failure - end -end - -module ConcurrentRubyConcurrencyHelpers +class ActionCable::TestCase < ActiveSupport::TestCase def wait_for_async wait_for_executor Concurrent.global_io_executor end @@ -56,18 +22,14 @@ module ConcurrentRubyConcurrencyHelpers yield wait_for_async end -end - -class ActionCable::TestCase < ActiveSupport::TestCase - if ENV["FAYE"].present? - include EventMachineConcurrencyHelpers - else - include ConcurrentRubyConcurrencyHelpers - end def wait_for_executor(executor) + # do not wait forever, wait 2s + timeout = 2 until executor.completed_task_count == executor.scheduled_task_count sleep 0.1 + timeout -= 0.1 + raise "Executor could not complete all tasks in 2 seconds" unless timeout > 0 end end end diff --git a/actioncable/test/worker_test.rb b/actioncable/test/worker_test.rb index 63dc453840..3385593f74 100644 --- a/actioncable/test/worker_test.rb +++ b/actioncable/test/worker_test.rb @@ -9,7 +9,7 @@ class WorkerTest < ActiveSupport::TestCase end def process(message) - @last_action = [ :process, message ] + @last_action = [ :process, message ] end def connection diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 3b9f503a0b..e488d867de 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,6 +1 @@ -* Exception handling: use `rescue_from` to handle exceptions raised by - mailer actions, by message delivery, and by deferred delivery jobs. - - *Jeremy Daer* - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 8573eb1225..ac810e86d0 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index e75dae6cf9..5eadd01407 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/actionmailer/bin/test b/actionmailer/bin/test index 84a05bba08..a7beb14b27 100755 --- a/actionmailer/bin/test +++ b/actionmailer/bin/test @@ -2,5 +2,3 @@ COMPONENT_ROOT = File.expand_path("..", __dir__) require File.expand_path("../tools/test", COMPONENT_ROOT) - -exit Minitest.run(ARGV) diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 668bc99435..8e59f033d0 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -25,6 +25,7 @@ require "abstract_controller" require "action_mailer/version" # Common Active Support usage in Action Mailer +require "active_support" require "active_support/rails" require "active_support/core_ext/class" require "active_support/core_ext/module/attr_internal" @@ -42,6 +43,7 @@ module ActionMailer autoload :DeliveryMethods autoload :InlinePreviewInterceptor autoload :MailHelper + autoload :Parameterized autoload :Preview autoload :Previews, "action_mailer/preview" autoload :TestCase diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index e9966c7ff5..7133670b65 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -133,6 +133,9 @@ module ActionMailer # # config.action_mailer.default_url_options = { host: "example.com" } # + # You can also define a <tt>default_url_options</tt> method on individual mailers to override these + # default settings per-mailer. + # # By default when <tt>config.force_ssl</tt> is true, URLs generated for hosts will use the HTTPS protocol. # # = Sending mail @@ -208,6 +211,19 @@ module ActionMailer # end # end # + # You can also send attachments with html template, in this case you need to add body, attachments, + # and custom content type like this: + # + # class NotifierMailer < ApplicationMailer + # def welcome(recipient) + # attachments["free_book.pdf"] = File.read("path/to/file.pdf") + # mail(to: recipient, + # subject: "New account information", + # content_type: "text/html", + # body: "<html><body>Hello there</body></html>") + # end + # end + # # = Inline Attachments # # You can also specify that a file should be displayed inline with other HTML. This is useful @@ -275,20 +291,19 @@ module ActionMailer # content_description: 'This is a description' # end # - # Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you - # can define methods that evaluate as the message is being generated: + # Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash, + # so you can define methods that evaluate as the message is being generated: # # class NotifierMailer < ApplicationMailer - # default 'X-Special-Header' => Proc.new { my_method } + # default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address } # # private - # # def my_method # 'some complex call' # end # end # - # Note that the proc is evaluated right at the start of the mail message generation, so if you + # Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you # set something in the default hash using a proc, and then set the same thing inside of your # mailer method, it will get overwritten by the mailer method. # @@ -311,7 +326,6 @@ module ActionMailer # end # # private - # # def add_inline_attachment! # attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg') # end @@ -417,10 +431,11 @@ module ActionMailer # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. # - # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. + # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+. class Base < AbstractController::Base include DeliveryMethods include Rescuable + include Parameterized include Previews abstract! @@ -444,8 +459,7 @@ module ActionMailer helper ActionMailer::MailHelper - class_attribute :default_params - self.default_params = { + class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", content_type: "text/plain", @@ -544,9 +558,9 @@ module ActionMailer end end - protected + private - def set_payload_for_mail(payload, mail) #:nodoc: + def set_payload_for_mail(payload, mail) payload[:mailer] = name payload[:message_id] = mail.message_id payload[:subject] = mail.subject @@ -558,7 +572,7 @@ module ActionMailer payload[:mail] = mail.encoded end - def method_missing(method_name, *args) # :nodoc: + def method_missing(method_name, *args) if action_methods.include?(method_name.to_s) MessageDelivery.new(self, method_name, *args) else @@ -566,10 +580,8 @@ module ActionMailer end end - private - - def respond_to_missing?(method, include_all = false) #:nodoc: - action_methods.include?(method.to_s) + def respond_to_missing?(method, include_all = false) + action_methods.include?(method.to_s) || super end end @@ -588,7 +600,8 @@ module ActionMailer def process(method_name, *args) #:nodoc: payload = { mailer: self.class.name, - action: method_name + action: method_name, + args: args } ActiveSupport::Notifications.instrument("process.action_mailer", payload) do @@ -601,7 +614,7 @@ module ActionMailer def body; "" end def header; {} end - def respond_to?(string, include_all=false) + def respond_to?(string, include_all = false) true end @@ -830,7 +843,7 @@ module ActionMailer message end - protected + private # Used by #mail to set the content type of the message. # @@ -841,7 +854,7 @@ module ActionMailer # If there is no content type passed in via headers, and there are no # attachments, or the message is multipart, then the default content type is # used. - def set_content_type(m, user_content_type, class_default) + def set_content_type(m, user_content_type, class_default) # :doc: params = m.content_type_parameters || {} case when user_content_type.present? @@ -863,23 +876,21 @@ module ActionMailer # If it does not find a translation for the +subject+ under the specified scope it will default to a # humanized version of the <tt>action_name</tt>. # If the subject has interpolations, you can pass them through the +interpolations+ parameter. - def default_i18n_subject(interpolations = {}) + def default_i18n_subject(interpolations = {}) # :doc: mailer_scope = self.class.mailer_name.tr("/", ".") I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize)) end # Emails do not support relative path links. - def self.supports_path? + def self.supports_path? # :doc: false end - private - def apply_defaults(headers) default_values = self.class.default.map do |key, value| [ key, - value.is_a?(Proc) ? instance_eval(&value) : value + value.is_a?(Proc) ? instance_exec(&value) : value ] end.to_h @@ -900,15 +911,19 @@ module ActionMailer yield(collector) collector.responses elsif headers[:body] - [{ - body: headers.delete(:body), - content_type: self.class.default[:content_type] || "text/plain" - }] + collect_responses_from_text(headers) else collect_responses_from_templates(headers) end end + def collect_responses_from_text(headers) + [{ + body: headers.delete(:body), + content_type: headers[:content_type] || "text/plain" + }] + end + def collect_responses_from_templates(headers) templates_path = headers[:template_path] || self.class.mailer_name templates_name = headers[:template_name] || action_name @@ -933,7 +948,7 @@ module ActionMailer def create_parts_from_responses(m, responses) if responses.size == 1 && !m.has_attachments? - responses[0].each { |k,v| m[k] = v } + responses[0].each { |k, v| m[k] = v } elsif responses.size > 1 && m.has_attachments? container = Mail::Part.new container.content_type = "multipart/alternative" @@ -959,7 +974,7 @@ module ActionMailer end def instrument_name - "action_mailer" + "action_mailer".freeze end ActiveSupport.run_load_hooks(:action_mailer, self) diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index 6be2c91da6..afdc28aa2a 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -7,8 +7,6 @@ module ActionMailer extend ActiveSupport::Concern included do - class_attribute :delivery_methods, :delivery_method - # Do not make this inheritable, because we always want it to propagate cattr_accessor :raise_delivery_errors self.raise_delivery_errors = true @@ -19,8 +17,8 @@ module ActionMailer cattr_accessor :deliver_later_queue_name self.deliver_later_queue_name = :mailers - self.delivery_methods = {}.freeze - self.delivery_method = :smtp + class_attribute :delivery_methods, default: {}.freeze + class_attribute :delivery_method, default: :smtp add_delivery_method :smtp, Mail::SMTP, address: "localhost", @@ -52,14 +50,14 @@ module ActionMailer # add_delivery_method :sendmail, Mail::Sendmail, # location: '/usr/sbin/sendmail', # arguments: '-i' - def add_delivery_method(symbol, klass, default_options={}) + def add_delivery_method(symbol, klass, default_options = {}) class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings") send(:"#{symbol}_settings=", default_options) self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze end - def wrap_delivery_behavior(mail, method=nil, options=nil) # :nodoc: - method ||= self.delivery_method + def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc: + method ||= delivery_method mail.delivery_handler = self case method diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index 7dafceef2b..f5594ef928 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -6,7 +6,7 @@ module ActionMailer module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 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 b7318f0092..980415afe0 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -11,7 +11,7 @@ module ActionMailer # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) # class InlinePreviewInterceptor - PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i + PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i include Base64 @@ -26,7 +26,7 @@ module ActionMailer def transform! #:nodoc: return message if html_part.blank? - html_source.gsub!(PATTERN) do |match| + html_part.body = html_part.decoded.gsub(PATTERN) do |match| if part = find_part(match[9..-2]) %[src="#{data_url(part)}"] else @@ -46,10 +46,6 @@ module ActionMailer @html_part ||= message.html_part end - def html_source - html_part.body.raw_source - end - def data_url(part) "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}" end diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index 994d297768..cf7c57e6bf 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -56,7 +56,7 @@ module ActionMailer # * <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={}) + def deliver_later!(options = {}) enqueue_delivery :deliver_now!, options end @@ -72,7 +72,7 @@ module ActionMailer # * <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={}) + def deliver_later(options = {}) enqueue_delivery :deliver_now, options end @@ -106,7 +106,7 @@ module ActionMailer end end - def enqueue_delivery(delivery_method, options={}) + def enqueue_delivery(delivery_method, options = {}) if processed? ::Kernel.raise "You've accessed the message before asking to " \ "deliver it later, so you may have made local changes that would " \ diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb new file mode 100644 index 0000000000..3acacc1f14 --- /dev/null +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -0,0 +1,152 @@ +module ActionMailer + # Provides the option to parameterize mailers in order to share instance variable + # setup, processing, and common headers. + # + # Consider this example that does not use parameterization: + # + # class InvitationsMailer < ApplicationMailer + # def account_invitation(inviter, invitee) + # @account = inviter.account + # @inviter = inviter + # @invitee = invitee + # + # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # + # def project_invitation(project, inviter, invitee) + # @account = inviter.account + # @project = project + # @inviter = inviter + # @invitee = invitee + # @summarizer = ProjectInvitationSummarizer.new(@project.bucket) + # + # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # + # def bulk_project_invitation(projects, inviter, invitee) + # @account = inviter.account + # @projects = projects.sort_by(&:name) + # @inviter = inviter + # @invitee = invitee + # + # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # end + # + # InvitationsMailer.account_invitation(person_a, person_b).deliver_later + # + # Using parameterized mailers, this can be rewritten as: + # + # class InvitationsMailer < ApplicationMailer + # before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + # before_action { @account = params[:inviter].account } + # + # default to: -> { @invitee.email_address }, + # from: -> { common_address(@inviter) }, + # reply_to: -> { @inviter.email_address_with_name } + # + # def account_invitation + # mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + # end + # + # def project_invitation + # @project = params[:project] + # @summarizer = ProjectInvitationSummarizer.new(@project.bucket) + # + # mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" + # end + # + # def bulk_project_invitation + # @projects = params[:projects].sort_by(&:name) + # + # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})" + # end + # end + # + # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later + module Parameterized + extend ActiveSupport::Concern + + included do + attr_accessor :params + end + + module ClassMethods + # Provide the parameters to the mailer in order to use them in the instance methods and callbacks. + # + # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later + # + # See Parameterized documentation for full example. + def with(params) + ActionMailer::Parameterized::Mailer.new(self, params) + end + end + + class Mailer # :nodoc: + def initialize(mailer, params) + @mailer, @params = mailer, params + end + + private + def method_missing(method_name, *args) + if @mailer.action_methods.include?(method_name.to_s) + ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args) + else + super + end + end + + def respond_to_missing?(method, include_all = false) + @mailer.respond_to?(method, include_all) + end + end + + class MessageDelivery < ActionMailer::MessageDelivery # :nodoc: + def initialize(mailer_class, action, params, *args) + super(mailer_class, action, *args) + @params = params + end + + private + def processed_mailer + @processed_mailer ||= @mailer_class.new.tap do |mailer| + mailer.params = @params + mailer.process @action, *@args + end + end + + def enqueue_delivery(delivery_method, options = {}) + if processed? + super + else + args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args + ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args) + end + end + end + + class DeliveryJob < ActionMailer::DeliveryJob # :nodoc: + def perform(mailer, mail_method, delivery_method, params, *args) + mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method) + end + end + end +end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 93a11453bf..87ba743f3d 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -52,6 +52,12 @@ module ActionMailer class Preview extend ActiveSupport::DescendantsTracker + attr_reader :params + + def initialize(params = {}) + @params = params + end + class << self # Returns all mailer preview classes. def all @@ -62,8 +68,8 @@ module ActionMailer # Returns the mail object for the given email name. The registered preview # interceptors will be informed so that they can transform the message # as they would if the mail was actually being delivered. - def call(email) - preview = self.new + def call(email, params = {}) + preview = new(params) message = preview.public_send(email) inform_preview_interceptors(message) message @@ -94,22 +100,22 @@ module ActionMailer name.sub(/Preview$/, "").underscore end - protected - def load_previews #:nodoc: + private + def load_previews if preview_path Dir["#{preview_path}/**/*_preview.rb"].each { |file| require_dependency file } end end - def preview_path #:nodoc: + def preview_path Base.preview_path end - def show_previews #:nodoc: + def show_previews Base.show_previews end - def inform_preview_interceptors(message) #:nodoc: + def inform_preview_interceptors(message) Base.preview_interceptors.each do |interceptor| interceptor.previewing_email(message) end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index c47d7781cc..913df8cf93 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -28,7 +28,7 @@ module ActionMailer options.cache_store ||= Rails.cache if options.show_previews - options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil + options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil end # make sure readers methods get compiled @@ -44,7 +44,7 @@ module ActionMailer register_preview_interceptors(options.delete(:preview_interceptors)) register_observers(options.delete(:observers)) - options.each { |k,v| send("#{k}=", v) } + options.each { |k, v| send("#{k}=", v) } end ActiveSupport.on_load(:action_dispatch_integration_test) { include ActionMailer::TestCase::ClearTestDeliveries } diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 1d09a4ee96..9ead03a40c 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -4,8 +4,8 @@ require "rails-dom-testing" module ActionMailer class NonInferrableMailerError < ::StandardError def initialize(name) - super "Unable to determine the mailer to test from #{name}. " + - "You'll need to specify it using tests YourMailer in your " + + super "Unable to determine the mailer to test from #{name}. " \ + "You'll need to specify it using tests YourMailer in your " \ "test case definition" end end @@ -57,7 +57,7 @@ module ActionMailer end def mailer_class - if mailer = self._mailer_class + if mailer = _mailer_class mailer else tests determine_default_mailer(name) @@ -73,38 +73,36 @@ module ActionMailer end end - protected + private - def initialize_test_deliveries # :nodoc: + def initialize_test_deliveries set_delivery_method :test @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.perform_deliveries = true ActionMailer::Base.deliveries.clear end - def restore_test_deliveries # :nodoc: + def restore_test_deliveries restore_delivery_method ActionMailer::Base.perform_deliveries = @old_perform_deliveries end - def set_delivery_method(method) # :nodoc: + def set_delivery_method(method) @old_delivery_method = ActionMailer::Base.delivery_method ActionMailer::Base.delivery_method = method end - def restore_delivery_method # :nodoc: + def restore_delivery_method ActionMailer::Base.deliveries.clear ActionMailer::Base.delivery_method = @old_delivery_method end - def set_expected_mail # :nodoc: + def set_expected_mail @expected = Mail.new @expected.content_type ["text", "plain", { "charset" => charset }] @expected.mime_version = "1.0" end - private - def charset "UTF-8" end diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index c17ecad4c6..c30fb1fc18 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -88,7 +88,7 @@ module ActionMailer # end # end def assert_enqueued_emails(number, &block) - assert_enqueued_jobs number, only: ActionMailer::DeliveryJob, &block + assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block end # Asserts that no emails are enqueued for later delivery. @@ -107,7 +107,7 @@ module ActionMailer # end # end def assert_no_enqueued_emails(&block) - assert_no_enqueued_jobs only: ActionMailer::DeliveryJob, &block + assert_no_enqueued_jobs only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block end end end diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 9dd7ee7a27..bc21b07109 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -1,7 +1,7 @@ module Rails module Generators class MailerGenerator < NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) argument :actions, type: :array, default: [], banner: "method method" @@ -11,7 +11,7 @@ module Rails template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb") in_root do - if self.behavior == :invoke && !File.exist?(application_mailer_file_name) + if behavior == :invoke && !File.exist?(application_mailer_file_name) template "application_mailer.rb", application_mailer_file_name end end @@ -19,12 +19,11 @@ module Rails hook_for :template_engine, :test_framework - protected - def file_name + private + def file_name # :doc: @_file_name ||= super.gsub(/_mailer/i, "") end - private def application_mailer_file_name @_application_mailer_file_name ||= if mountable_engine? "app/mailers/#{namespaced_path}/application_mailer.rb" diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 82f03f5cba..dbfdb07e6e 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -3,13 +3,13 @@ require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end module Rails def self.root - File.expand_path("../", File.dirname(__FILE__)) + File.expand_path("..", __dir__) end end @@ -28,18 +28,18 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -FIXTURE_LOAD_PATH = File.expand_path("fixtures", File.dirname(__FILE__)) +FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb index 820d7f34a7..bf14fe0853 100644 --- a/actionmailer/test/assert_select_email_test.rb +++ b/actionmailer/test/assert_select_email_test.rb @@ -11,8 +11,8 @@ class AssertSelectEmailTest < ActionMailer::TestCase class AssertMultipartSelectMailer < ActionMailer::Base def test(options) mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format| - format.text { render text: options[:text] } - format.html { render text: options[:html] } + format.text { render plain: options[:text] } + format.html { render plain: options[:html] } end end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 3bca69890d..ebb97078e6 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -140,6 +140,11 @@ class BaseTest < ActiveSupport::TestCase assert_equal("multipart/mixed", email.mime_type) end + test "set mime type to text/html when attachment is included and body is set" do + email = BaseMailer.attachment_with_content(body: "Hello there", content_type: "text/html") + assert_equal("text/html", email.mime_type) + end + test "adds the rendered template as part" do email = BaseMailer.attachment_with_content assert_equal(2, email.parts.length) @@ -829,7 +834,26 @@ class BaseTest < ActiveSupport::TestCase assert_equal "special indeed!", mail["X-Special-Header"].to_s end - protected + test "notification for process" do + begin + events = [] + ActiveSupport::Notifications.subscribe("process.action_mailer") do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + BaseMailer.welcome(body: "Hello there").deliver_now + + assert_equal 1, events.length + assert_equal "process.action_mailer", events[0].name + assert_equal "BaseMailer", events[0].payload[:mailer] + assert_equal :welcome, events[0].payload[:action] + assert_equal [{ body: "Hello there" }], events[0].payload[:args] + ensure + ActiveSupport::Notifications.unsubscribe "process.action_mailer" + end + end + + private # Execute the block setting the given values and restoring old values after # the block is executed. @@ -944,3 +968,19 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase end end end + +class BasePreviewTest < ActiveSupport::TestCase + class BaseMailerPreview < ActionMailer::Preview + def welcome + BaseMailer.welcome(params) + end + end + + test "has access to params" do + params = { name: "World" } + + assert_called_with(BaseMailer, :welcome, [params]) do + BaseMailerPreview.call(:welcome, params) + end + end +end diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index cff49c8894..e76466439e 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -5,7 +5,7 @@ require "mailers/caching_mailer" CACHE_DIR = "test_cache" # Don't change '/../temp/' cavalierly or you might hose something you don't want hosed -FILE_STORE_PATH = File.join(File.dirname(__FILE__), "/../temp/", CACHE_DIR) +FILE_STORE_PATH = File.join(__dir__, "/../temp/", CACHE_DIR) class FragmentCachingMailer < ActionMailer::Base abstract! @@ -21,10 +21,6 @@ class BaseCachingTest < ActiveSupport::TestCase @mailer.perform_caching = true @mailer.cache_store = @store end - - def test_fragment_cache_key - assert_equal "views/what a key", @mailer.fragment_cache_key("what a key") - end end class FragmentCachingTest < BaseCachingTest @@ -126,7 +122,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match expected_body, email.body.encoded assert_match expected_body, - @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}") + @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}/caching") end def test_fragment_caching_in_partials @@ -135,7 +131,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match(expected_body, email.body.encoded) assert_match(expected_body, - @store.read("views/caching/#{template_digest("caching_mailer/_partial")}")) + @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial")}/caching")) end def test_skip_fragment_cache_digesting @@ -185,7 +181,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest end assert_equal "caching_mailer", payload[:mailer] - assert_equal "views/caching/#{template_digest("caching_mailer/fragment_cache")}", payload[:key] + assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}", :caching ], payload[:key] ensure @mailer.enable_fragment_cache_logging = true end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 898d32c1e2..f64a69019f 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -87,7 +87,7 @@ class MailDeliveryTest < ActiveSupport::TestCase from: "jose@test.plataformatec.com" } - def welcome(hash={}) + def welcome(hash = {}) mail(DEFAULT_HEADERS.merge(hash)) end end diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index 6370213043..4f09339800 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -18,7 +18,7 @@ end class TestController < ActionController::Base def send_mail email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver_now - render text: "Mail sent - Subject: #{email.subject}" + render plain: "Mail sent - Subject: #{email.subject}" end end @@ -57,16 +57,14 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance| assert_called(instance, :deliver!) do with_translation "de", email_subject: "[Anmeldung] Willkommen" do - ActiveSupport::Deprecation.silence do - get "/test/send_mail" - end + get "/test/send_mail" assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body end end end end - protected + private def with_translation(locale, data) I18n.backend.store_translations(locale, data) diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index d864c3acca..799c6144d7 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -26,7 +26,7 @@ class AMLogSubscriberTest < ActionMailer::TestCase wait assert_equal(1, @logger.logged(:info).size) - assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first) + assert_match(/Sent mail to system@test\.lindsaar\.net/, @logger.logged(:info).first) assert_equal(2, @logger.logged(:debug).size) assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first) @@ -36,7 +36,7 @@ class AMLogSubscriberTest < ActionMailer::TestCase end def test_receive_is_notified - fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email") + fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) TestMailer.receive(fixture) wait assert_equal(1, @logger.logged(:info).size) diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index 972629e477..6042548aef 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -2,11 +2,11 @@ require "abstract_unit" class HelperMailer < ActionMailer::Base def use_mail_helper - @text = "But soft! What light through yonder window breaks? It is the east, " + - "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " + - "which is sick and pale with grief that thou, her maid, art far more " + - "fair than she. Be not her maid, for she is envious! Her vestal " + - "livery is but sick and green, and none but fools do wear it. Cast " + + @text = "But soft! What light through yonder window breaks? It is the east, " \ + "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " \ + "which is sick and pale with grief that thou, her maid, art far more " \ + "fair than she. Be not her maid, for she is envious! Her vestal " \ + "livery is but sick and green, and none but fools do wear it. Cast " \ "it off!" mail_with_defaults do |format| @@ -65,7 +65,7 @@ The second end end - protected + private def mail_with_defaults(&block) mail(to: "test@localhost", from: "tester@example.com", diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index 8ced74c214..2a8884959c 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -62,8 +62,8 @@ class BaseMailer < ActionMailer::Base def explicit_multipart(hash = {}) attachments["invoice.pdf"] = "This is test File content" if hash.delete(:attachments) mail(hash) do |format| - format.text { render text: "TEXT Explicit Multipart" } - format.html { render text: "HTML Explicit Multipart" } + format.text { render plain: "TEXT Explicit Multipart" } + format.html { render plain: "HTML Explicit Multipart" } end end @@ -76,7 +76,7 @@ class BaseMailer < ActionMailer::Base def explicit_multipart_with_any(hash = {}) mail(hash) do |format| - format.any(:text, :html) { render text: "Format with any!" } + format.any(:text, :html) { render plain: "Format with any!" } end end @@ -100,18 +100,18 @@ class BaseMailer < ActionMailer::Base end end - def implicit_different_template(template_name="") + def implicit_different_template(template_name = "") mail(template_name: template_name) end - def explicit_different_template(template_name="") + def explicit_different_template(template_name = "") mail do |format| format.text { render template: "#{mailer_name}/#{template_name}" } format.html { render template: "#{mailer_name}/#{template_name}" } end end - def different_layout(layout_name="") + def different_layout(layout_name = "") mail do |format| format.text { render layout: layout_name } format.html { render layout: layout_name } diff --git a/actionmailer/test/mailers/params_mailer.rb b/actionmailer/test/mailers/params_mailer.rb new file mode 100644 index 0000000000..4c0fae6d91 --- /dev/null +++ b/actionmailer/test/mailers/params_mailer.rb @@ -0,0 +1,11 @@ +class ParamsMailer < ActionMailer::Base + before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + + default to: Proc.new { @invitee }, from: -> { @inviter } + + def invitation + mail(subject: "Welcome to the project!") do |format| + format.text { render plain: "So says #{@inviter}" } + end + end +end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index 0eb81a8496..c0683be94d 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -76,14 +76,14 @@ class MessageDeliveryTest < ActiveSupport::TestCase test "should enqueue a delivery with a delay" do travel_to Time.new(2004, 11, 24, 01, 04, 44) do - assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current.to_f+600.seconds, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do - @mail.deliver_later wait: 600.seconds + assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do + @mail.deliver_later wait: 10.minutes end end end test "should enqueue a delivery at a specific time" do - later_time = Time.now.to_f + 3600 + later_time = Time.current + 1.hour assert_performed_with(job: ActionMailer::DeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do @mail.deliver_later wait_until: later_time end diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb new file mode 100644 index 0000000000..e988fffcb9 --- /dev/null +++ b/actionmailer/test/parameterized_test.rb @@ -0,0 +1,54 @@ +require "abstract_unit" +require "active_job" +require "mailers/params_mailer" + +class ParameterizedTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + setup do + @previous_logger = ActiveJob::Base.logger + ActiveJob::Base.logger = Logger.new(nil) + + @previous_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + + @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name + ActionMailer::Base.deliver_later_queue_name = :test_queue + + @mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation + end + + teardown do + ActiveJob::Base.logger = @previous_logger + ParamsMailer.deliveries.clear + + ActionMailer::Base.delivery_method = @previous_delivery_method + ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name + end + + test "parameterized headers" do + assert_equal(["jason@basecamp.com"], @mail.to) + assert_equal(["david@basecamp.com"], @mail.from) + assert_equal("So says david@basecamp.com", @mail.body.encoded) + end + + test "enqueue the email with params" do + assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do + @mail.deliver_later + end + end + + test "respond_to?" do + mailer = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com") + + assert_respond_to mailer, :invitation + assert_not_respond_to mailer, :anything + + invitation = mailer.method(:invitation) + assert_equal Method, invitation.class + + assert_raises(NameError) do + invitation = mailer.method(:anything) + end + end +end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 31ac5a5211..876e9b0634 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -143,6 +143,16 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_enqueued_parameterized_emails + assert_nothing_raised do + assert_enqueued_emails 1 do + silence_stream($stdout) do + TestHelperMailer.with(a: 1).test.deliver_later + end + end + end + end + def test_assert_enqueued_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_emails 2 do @@ -176,6 +186,14 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_no_enqueued_parameterized_emails + assert_nothing_raised do + assert_no_enqueued_emails do + TestHelperMailer.with(a: 1).test.deliver_now + end + end + end + def test_assert_no_enqueued_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_enqueued_emails do diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 27f6e8a491..6dbfb3a1ff 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -50,11 +50,11 @@ class ActionMailerUrlTest < ActionMailer::TestCase end end - def encode( text, charset="UTF-8" ) - quoted_printable( text, charset ) + def encode(text, charset = "UTF-8") + quoted_printable(text, charset) end - def new_mail( charset="UTF-8" ) + def new_mail(charset = "UTF-8") mail = Mail.new mail.mime_version = "1.0" if charset @@ -81,7 +81,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase AppRoutes.draw do ActiveSupport::Deprecation.silence do get ":controller(/:action(/:id))" - get "/welcome" => "foo#bar", as: "welcome" + get "/welcome" => "foo#bar", as: "welcome" get "/dummy_model" => "foo#baz", as: "dummy_model" end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index e7b8e1b628..54937617df 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,139 +1,37 @@ -* Fix adding implicitly rendered template digests to ETags. +* AEAD encrypted cookies and sessions with GCM - Fixes a case when modifying an implicitly rendered template for a - controller action using `fresh_when` or `stale?` would not result in a new - `ETag` value. + 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. - *Javan Makhmali* + *Michael J Coyne* -* Make `fixture_file_upload` work in integration tests. +* Change the cache key format for fragments to make it easier to debug key churn. The new format is: - *Yuji Yaginuma* - -* Add `to_param` to `ActionController::Parameters` deprecations. - - In the future `ActionController::Parameters` are discouraged from being used - in URLs without explicit whitelisting. Go through `to_h` to use `to_param`. - - *Kir Shatrov* - -* Fix nested multiple roots - - The PR #20940 enabled the use of multiple roots with different constraints - at the top level but unfortunately didn't work when those roots were inside - a namespace and also broke the use of root inside a namespace after a top - level root was defined because the check for the existence of the named route - used the global :root name and not the namespaced name. - - This is fixed by using the name_for_action method to expand the :root name to - the full namespaced name. We can pass nil for the second argument as we're not - dealing with resource definitions so don't need to handle the cases for edit - and new routes. - - Fixes #26148. - - *Ryo Hashimoto*, *Andrew White* - -* Include the content of the flash in the auto-generated etag. This solves the following problem: - - 1. POST /messages - 2. redirect_to messages_url, notice: 'Message was created' - 3. GET /messages/1 - 4. GET /messages - - Step 4 would before still include the flash message, even though it's no longer relevant, - because the etag cache was recorded with the flash in place and didn't change when it was gone. + views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + ^template path ^template tree digest ^class ^id *DHH* -* SSL: Changes redirect behavior for all non-GET and non-HEAD requests - (like POST/PUT/PATCH etc) to `http://` resources to redirect to `https://` - with a [307 status code](http://tools.ietf.org/html/rfc7231#section-6.4.7) instead of [301 status code](http://tools.ietf.org/html/rfc7231#section-6.4.2). - - 307 status code instructs the HTTP clients to preserve the original - request method while redirecting. It has been part of HTTP RFC since - 1999 and is implemented/recognized by most (if not all) user agents. - - # Before - POST http://example.com/articles (i.e. ArticlesContoller#create) - redirects to - GET https://example.com/articles (i.e. ArticlesContoller#index) - - # After - POST http://example.com/articles (i.e. ArticlesContoller#create) - redirects to - POST https://example.com/articles (i.e. ArticlesContoller#create) - - *Chirag Singhal* - -* Add `:as` option to `ActionController:TestCase#process` and related methods. - - Specifying `as: mime_type` allows the `CONTENT_TYPE` header to be specified - in controller tests without manually doing this through `@request.headers['CONTENT_TYPE']`. +* 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. - *Everest Stefan Munro-Zeisberger* - -* Show cache hits and misses when rendering partials. - - Partials using the `cache` helper will show whether a render hit or missed - the cache: - - ``` - Rendered messages/_message.html.erb in 1.2 ms [cache hit] - Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] - ``` - - This removes the need for the old fragment cache logging: - - ``` - Read fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/d0bdf2974e1ef6d31685c3b392ad0b74 (0.6ms) - Rendered messages/_message.html.erb in 1.2 ms [cache hit] - Write fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/3b4e249ac9d168c617e32e84b99218b5 (1.1ms) - Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] - ``` - - Though that full output can be reenabled with - `config.action_controller.enable_fragment_cache_logging = true`. - - *Stan Lo* - -* Don't override the `Accept` header in integration tests when called with `xhr: true`. - - Fixes #25859. - - *David Chen* - -* Fix `defaults` option for root route. - - A regression from some refactoring for the 5.0 release, this change - fixes the use of `defaults` (default parameters) in the `root` routing method. - - *Chris Arcand* - -* Check `request.path_parameters` encoding at the point they're set. - - Check for any non-UTF8 characters in path parameters at the point they're - set in `env`. Previously they were checked for when used to get a controller - class, but this meant routes that went directly to a Rack app, or skipped - controller instantiation for some other reason, had to defend against - non-UTF8 characters themselves. - - *Grey Baker* + *DHH* -* Don't raise `ActionController::UnknownHttpMethod` from `ActionDispatch::Static`. +* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` - Pass `Rack::Request` objects to `ActionDispatch::FileHandler` to avoid it - raising `ActionController::UnknownHttpMethod`. If an unknown method is - passed, it should pass exception higher in the stack instead, once we've had a - chance to define exception handling behaviour. + `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. - *Grey Baker* + This is fixed by adding two new hooks so you can target `ActionController::Base` vs `ActionController::API` -* Handle `Rack::QueryParser` errors in `ActionDispatch::ExceptionWrapper`. + Fixes #27013. - Updated `ActionDispatch::ExceptionWrapper` to handle the Rack 2.0 namespace - for `ParameterTypeError` and `InvalidParameterError` errors. + *Julian Nadeau* - *Grey Baker* -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 8573eb1225..ac810e86d0 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/Rakefile b/actionpack/Rakefile index 31dd1865f9..69408c8aab 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -26,7 +26,7 @@ namespace :test do end task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.expand_path("..", __dir__) + "/tools/line_statistics" files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 2c24a54305..31803042dd 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/actionpack/bin/test b/actionpack/bin/test index 84a05bba08..a7beb14b27 100755 --- a/actionpack/bin/test +++ b/actionpack/bin/test @@ -2,5 +2,3 @@ COMPONENT_ROOT = File.expand_path("..", __dir__) require File.expand_path("../tools/test", COMPONENT_ROOT) - -exit Minitest.run(ARGV) diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 8e588812f8..dc79820a82 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,4 +1,3 @@ -require "erubis" require "abstract_controller/error" require "active_support/configurable" require "active_support/descendants_tracker" @@ -15,14 +14,21 @@ module AbstractController # expected to provide their own +render+ method, since rendering means # different things depending on the context. class Base + ## + # Returns the body of the HTTP response sent by the controller. attr_internal :response_body + + ## + # Returns the name of the action this controller is processing. attr_internal :action_name + + ## + # Returns the formats that can be processed by the controller. attr_internal :formats include ActiveSupport::Configurable extend ActiveSupport::DescendantsTracker - undef_method :not_implemented class << self attr_reader :abstract alias_method :abstract?, :abstract @@ -215,7 +221,7 @@ module AbstractController # ==== Returns # * <tt>string</tt> - The name of the method that handles the action # * false - No valid method name could be found. - # Raise AbstractController::ActionNotFound. + # Raise +AbstractController::ActionNotFound+. def _find_action_name(action_name) _valid_action_name?(action_name) && method_for_action(action_name) end @@ -231,11 +237,11 @@ module AbstractController # with a template matching the action name is considered to exist. # # If you override this method to handle additional cases, you may - # also provide a method (like _handle_method_missing) to handle + # also provide a method (like +_handle_method_missing+) to handle # the case. # - # If none of these conditions are true, and method_for_action - # returns nil, an AbstractController::ActionNotFound exception will be raised. + # If none of these conditions are true, and +method_for_action+ + # returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised. # # ==== Parameters # * <tt>action_name</tt> - An action name to find a method name for diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb index d222880922..30e3d4426c 100644 --- a/actionpack/lib/abstract_controller/caching.rb +++ b/actionpack/lib/abstract_controller/caching.rb @@ -37,8 +37,7 @@ module AbstractController config_accessor :enable_fragment_cache_logging self.enable_fragment_cache_logging = false - class_attribute :_view_cache_dependencies - self._view_cache_dependencies = [] + class_attribute :_view_cache_dependencies, default: [] helper_method :view_cache_dependencies if respond_to?(:helper_method) end @@ -52,9 +51,9 @@ module AbstractController self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact end - protected + private # Convenience accessor. - def cache(key, options = {}, &block) + def cache(key, options = {}, &block) # :doc: if cache_configured? cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) else diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index c85b4adba1..14e4a82523 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -25,7 +25,10 @@ module AbstractController self.fragment_cache_keys = [] - helper_method :fragment_cache_key if respond_to?(:helper_method) + if respond_to?(:helper_method) + helper_method :fragment_cache_key + helper_method :combined_fragment_cache_key + end end module ClassMethods @@ -62,17 +65,36 @@ module AbstractController # with the specified +key+ value. The key is expanded using # ActiveSupport::Cache.expand_cache_key. def fragment_cache_key(key) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0. + All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array, + such that the caching stores can interrogate the parts for cache versions used in + recyclable cache keys. + MSG + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) end + # Given a key (as described in +expire_fragment+), returns + # a key array suitable for use in reading, writing, or expiring a + # cached fragment. All keys begin with <tt>:views</tt>, + # followed by ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] if set, + # followed by any controller-wide key prefix values, ending + # with the specified +key+ value. + def combined_fragment_cache_key(key) + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } + tail = key.is_a?(Hash) ? url_for(key).split("://").last : key + [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact + end + # Writes +content+ to the location signified by # +key+ (see +expire_fragment+ for acceptable formats). def write_fragment(key, content, options = nil) return content unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :write_fragment, key do content = content.to_str cache_store.write(key, content, options) @@ -85,7 +107,7 @@ module AbstractController def read_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :read_fragment, key do result = cache_store.read(key, options) result.respond_to?(:html_safe) ? result.html_safe : result @@ -96,7 +118,7 @@ module AbstractController # +key+ exists (see +expire_fragment+ for acceptable formats). def fragment_exist?(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :exist_fragment?, key do cache_store.exist?(key, options) @@ -123,7 +145,7 @@ module AbstractController # method (or <tt>delete_matched</tt>, for Regexp keys). def expire_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) unless key.is_a?(Regexp) + key = combined_fragment_cache_key(key) unless key.is_a?(Regexp) instrument_fragment_cache :expire_fragment, key do if key.is_a?(Regexp) @@ -135,8 +157,7 @@ module AbstractController end def instrument_fragment_cache(name, key) # :nodoc: - payload = instrument_payload(key) - ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield } + ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield } end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 73775e12c2..ba7dec6083 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,4 +1,24 @@ module AbstractController + # = Abstract Controller Callbacks + # + # Abstract Controller provides hooks during the life cycle of a controller action. + # Callbacks allow you to trigger logic during this cycle. Available callbacks are: + # + # * <tt>after_action</tt> + # * <tt>append_after_action</tt> + # * <tt>append_around_action</tt> + # * <tt>append_before_action</tt> + # * <tt>around_action</tt> + # * <tt>before_action</tt> + # * <tt>prepend_after_action</tt> + # * <tt>prepend_around_action</tt> + # * <tt>prepend_before_action</tt> + # * <tt>skip_after_action</tt> + # * <tt>skip_around_action</tt> + # * <tt>skip_before_action</tt> + # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks extend ActiveSupport::Concern @@ -54,25 +74,6 @@ module AbstractController end end - # Skip before, after, and around action callbacks matching any of the names. - # - # ==== Parameters - # * <tt>names</tt> - A list of valid names that could be used for - # callbacks. Note that skipping uses Ruby equality, so it's - # impossible to skip a callback defined using an anonymous proc - # using #skip_action_callback. - def skip_action_callback(*names) - ActiveSupport::Deprecation.warn("`skip_action_callback` is deprecated and will be removed in Rails 5.1. Please use skip_before_action, skip_after_action or skip_around_action instead.") - skip_before_action(*names, raise: false) - skip_after_action(*names, raise: false) - skip_around_action(*names, raise: false) - end - - def skip_filter(*names) - ActiveSupport::Deprecation.warn("`skip_filter` is deprecated and will be removed in Rails 5.1. Use skip_before_action, skip_after_action or skip_around_action instead.") - skip_action_callback(*names) - end - # Take callback names and an optional callback proc, normalize them, # then call the block with each callback. This allows us to abstract # the normalization across several methods that use it. @@ -187,22 +188,12 @@ module AbstractController end end - define_method "#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will be removed in Rails 5.1. Use #{callback}_action instead.") - send("#{callback}_action", *names, &blk) - end - define_method "prepend_#{callback}_action" do |*names, &blk| _insert_callbacks(names, blk) do |name, options| set_callback(:process_action, callback, name, options.merge(prepend: true)) end end - define_method "prepend_#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use prepend_#{callback}_action instead.") - send("prepend_#{callback}_action", *names, &blk) - end - # Skip a before, after or around callback. See _insert_callbacks # for details on the allowed parameters. define_method "skip_#{callback}_action" do |*names| @@ -211,18 +202,8 @@ module AbstractController end end - define_method "skip_#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use skip_#{callback}_action instead.") - send("skip_#{callback}_action", *names, &blk) - end - # *_action is the same as append_*_action alias_method :"append_#{callback}_action", :"#{callback}_action" - - define_method "append_#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use append_#{callback}_action instead.") - send("append_#{callback}_action", *names, &blk) - end end end end diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index 57714b0588..40ae5aa1ca 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -19,7 +19,7 @@ module AbstractController generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym) end - protected + private def method_missing(symbol, &block) unless mime_constant = Mime[symbol] diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index ef3be7af83..2e50637c39 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -5,11 +5,8 @@ module AbstractController extend ActiveSupport::Concern included do - class_attribute :_helpers - self._helpers = Module.new - - class_attribute :_helper_methods - self._helper_methods = Array.new + class_attribute :_helpers, default: Module.new + class_attribute :_helper_methods, default: Array.new end class MissingHelperError < LoadError diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 2cb22cb53d..54af938a93 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,6 +1,4 @@ require "abstract_controller/error" -require "active_support/concern" -require "active_support/core_ext/class/attribute" require "action_view" require "action_view/view_paths" require "set" @@ -80,7 +78,7 @@ module AbstractController # <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to # <tt>render :file => "foo/bar"</tt>. # :api: plugin - def _normalize_args(action=nil, options={}) + def _normalize_args(action = nil, options = {}) if action.respond_to?(:permitted?) if action.permitted? action @@ -111,6 +109,9 @@ module AbstractController def _process_format(format) end + def _process_variant(options) + end + def _set_html_content_type # :nodoc: end @@ -121,10 +122,7 @@ module AbstractController # :api: private def _normalize_render(*args, &block) options = _normalize_args(*args, &block) - #TODO: remove defined? when we restore AP <=> AV dependency - if defined?(request) && !request.nil? && request.variant.present? - options[:variant] = request.variant - end + _process_variant(options) _normalize_options(options) options end diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 5cd8d77ddb..94698df730 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -81,10 +81,9 @@ module ActionController # end # end # - # Quite straightforward. Make sure to check the modules included in - # <tt>ActionController::Base</tt> if you want to use any other - # functionality that is not provided by <tt>ActionController::API</tt> - # out of the box. + # Make sure to check the modules included in <tt>ActionController::Base</tt> + # if you want to use any other functionality that is not provided + # by <tt>ActionController::API</tt> out of the box. class API < Metal abstract! @@ -142,6 +141,7 @@ module ActionController include mod end + ActiveSupport.run_load_hooks(:action_controller_api, self) ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index ca8066cd82..8c2b111f89 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -8,7 +8,7 @@ module ActionController # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. # # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other - # controllers in turn inherit from ApplicationController. This gives you one class to configure things such as + # controllers inherit from ApplicationController. This gives you one class to configure things such as # request forgery protection and filtering of sensitive request parameters. # # A sample controller could look like this: @@ -30,7 +30,7 @@ module ActionController # # Unlike index, the create action will not render a template. After performing its main purpose (creating a # new post), it initiates a redirect instead. This redirect works by returning an external - # "302 Moved" HTTP response that takes the user to the index action. + # <tt>302 Moved</tt> HTTP response that takes the user to the index action. # # These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect. # Most actions are variations on these themes. @@ -59,7 +59,7 @@ module ActionController # <input type="text" name="post[name]" value="david"> # <input type="text" name="post[address]" value="hyacintvej"> # - # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. + # A request coming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. # If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # @@ -74,7 +74,7 @@ module ActionController # # session[:person] = Person.authenticate(user_name, password) # - # And retrieved again through the same hash: + # You can retrieve it again through the same hash: # # Hello #{session[:person]} # @@ -261,6 +261,13 @@ module ActionController PROTECTED_IVARS end + def self.make_response!(request) + ActionDispatch::Response.create.tap do |res| + res.request = request + end + end + + ActiveSupport.run_load_hooks(:action_controller_base, self) ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index a9a8508abc..954265ad97 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -38,7 +38,7 @@ module ActionController end def instrument_name - "action_controller" + "action_controller".freeze end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index d29a5fe68f..d00fcbcd13 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -60,9 +60,9 @@ module ActionController class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(event) return unless logger.info? && ActionController::Base.enable_fragment_cache_logging - key_or_path = event.payload[:key] || event.payload[:path] + key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path]) human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)") + info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)") end METHOD end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 075e4504c2..96c708f45a 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -55,7 +55,7 @@ module ActionController list = except end - Middleware.new(get_class(klass), args, list, strategy, block) + Middleware.new(klass, args, list, strategy, block) end end @@ -118,11 +118,6 @@ module ActionController class Metal < AbstractController::Base abstract! - def env - @_request.env - end - deprecate :env - # Returns the last part of the controller's name, underscored, without the ending # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well. @@ -134,16 +129,16 @@ module ActionController end def self.make_response!(request) - ActionDispatch::Response.create.tap do |res| + ActionDispatch::Response.new.tap do |res| res.request = request end end - def self.encoding_for_param(action, param) # :nodoc: - ::Encoding::UTF_8 + def self.binary_params_for?(action) # :nodoc: + false end - # Delegates to the class' <tt>controller_name</tt> + # Delegates to the class' <tt>controller_name</tt>. def controller_name self.class.controller_name end @@ -213,8 +208,7 @@ module ActionController @_request.reset_session end - class_attribute :middleware_stack - self.middleware_stack = ActionController::MiddlewareStack.new + class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new def self.inherited(base) # :nodoc: base.middleware_stack = middleware_stack.dup @@ -232,14 +226,6 @@ module ActionController middleware_stack end - # Makes the controller a Rack endpoint that runs the action in the given - # +env+'s +action_dispatch.request.path_parameters+ key. - def self.call(env) - req = ActionDispatch::Request.new env - action(req.path_parameters[:action]).call(env) - end - class << self; deprecate :call; end - # Returns a Rack endpoint for the given action name. def self.action(name) if middleware_stack.any? @@ -257,7 +243,7 @@ module ActionController end end - # Direct dispatch to the controller. Instantiates the controller, then + # Direct dispatch to the controller. Instantiates the controller, then # executes the action named +name+. def self.dispatch(name, req, res) if middleware_stack.any? diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 89bf60a0bb..0525252c7c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -7,8 +7,7 @@ module ActionController include Head included do - class_attribute :etaggers - self.etaggers = [] + class_attribute :etaggers, default: [] end module ClassMethods @@ -238,7 +237,7 @@ module ActionController ) options.delete(:private) - response.cache_control[:extras] = options.map { |k,v| "#{k}=#{v}" } + response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" } response.date = Time.now unless response.date? end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index f089c8423b..731e03e2fc 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -11,7 +11,7 @@ module ActionController #:nodoc: DEFAULT_SEND_FILE_TYPE = "application/octet-stream".freeze #:nodoc: DEFAULT_SEND_FILE_DISPOSITION = "attachment".freeze #:nodoc: - protected + private # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via # +config.action_dispatch.x_sendfile_header+. @@ -70,7 +70,6 @@ module ActionController #:nodoc: send_file_headers! options self.status = options[:status] || 200 - self.content_type = options[:type] if options.key?(:type) self.content_type = options[:content_type] if options.key?(:content_type) response.send_file path end @@ -109,10 +108,12 @@ module ActionController #:nodoc: render options.slice(:status, :content_type).merge(body: data) end - private def send_file_headers!(options) type_provided = options.has_key?(:type) + self.content_type = DEFAULT_SEND_FILE_TYPE + response.sending_file = true + content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE) raise ArgumentError, ":type option required" if content_type.nil? @@ -137,8 +138,6 @@ module ActionController #:nodoc: headers["Content-Transfer-Encoding"] = "binary" - response.sending_file = true - # Fix a problem with IE 6.0 on opening downloaded files: # If Cache-Control: no-cache is set (which Rails does by default), # IE removes the file it just downloaded from its cache immediately diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb index 474d75f02e..7bd338bd7c 100644 --- a/actionpack/lib/action_controller/metal/etag_with_flash.rb +++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb @@ -1,9 +1,9 @@ module ActionController # When you're using the flash, it's generally used as a conditional on the view. # This means the content of the view depends on the flash. Which in turn means - # that the etag for a response should be computed with the content of the flash + # that the ETag for a response should be computed with the content of the flash # in mind. This does that by including the content of the flash as a component - # in the etag that's generated for a response. + # in the ETag that's generated for a response. module EtagWithFlash extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 6c103bb042..69c3979a0e 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -22,8 +22,7 @@ module ActionController include ActionController::ConditionalGet included do - class_attribute :etag_with_template_digest - self.etag_with_template_digest = true + class_attribute :etag_with_template_digest, default: true ActiveSupport.on_load :action_view, yield: true do etag do |options| @@ -40,7 +39,7 @@ module ActionController end # Pick the template digest to include in the ETag. If the +:template+ option - # is present, use the named template. If +:template+ is nil or absent, use + # is present, use the named template. If +:template+ is +nil+ or absent, use # the default controller/action template. If +:template+ is false, omit the # template digest from the ETag. def pick_template_for_etag(options) diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 56a4b085e2..175dd9eb9e 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -3,20 +3,10 @@ module ActionController end class BadRequest < ActionControllerError #:nodoc: - def initialize(msg = nil, e = nil) - if e - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize(msg = nil) super(msg) set_backtrace $!.backtrace if $! end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end class RenderError < ActionControllerError #:nodoc: @@ -24,7 +14,7 @@ module ActionController class RoutingError < ActionControllerError #:nodoc: attr_reader :failures - def initialize(message, failures=[]) + def initialize(message, failures = []) super(message) @failures = failures end diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 65351284b9..24d1097ebe 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -3,8 +3,7 @@ module ActionController #:nodoc: extend ActiveSupport::Concern included do - class_attribute :_flash_types, instance_accessor: false - self._flash_types = [] + class_attribute :_flash_types, instance_accessor: false, default: [] delegate :flash, to: :request add_flash_types(:alert, :notice) @@ -42,7 +41,7 @@ module ActionController #:nodoc: end end - protected + private def redirect_to(options = {}, response_status_and_flash = {}) #:doc: self.class._flash_types.each do |flash_type| if type = response_status_and_flash.delete(flash_type) diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index b8976497a4..73e67573ca 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -2,17 +2,17 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" module ActionController - # This module provides a method which will redirect the browser to use HTTPS - # protocol. This will ensure that user's sensitive information will be + # This module provides a method which will redirect the browser to use the secured HTTPS + # protocol. This will ensure that users' sensitive information will be # transferred safely over the internet. You _should_ always force the browser # to use HTTPS when you're transferring sensitive information such as # user authentication, account information, or credit card information. # # Note that if you are really concerned about your application security, # you might consider using +config.force_ssl+ in your config file instead. - # That will ensure all the data transferred via HTTPS protocol and prevent - # the user from getting their session hijacked when accessing the site over - # unsecured HTTP protocol. + # That will ensure all the data is transferred via HTTPS, and will + # prevent the user from getting their session hijacked when accessing the + # site over unsecured HTTP protocol. module ForceSSL extend ActiveSupport::Concern include AbstractController::Callbacks @@ -23,7 +23,7 @@ module ActionController module ClassMethods # Force the request to this particular controller or specified actions to be - # under HTTPS protocol. + # through the HTTPS protocol. # # If you need to disable this for any reason (e.g. development) then you can use # an +:if+ or +:unless+ condition. @@ -71,7 +71,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 & + # * <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? @@ -89,7 +89,7 @@ module ActionController end secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS)) - flash.keep if respond_to?(:flash) + flash.keep if respond_to?(:flash) && request.respond_to?(:flash) redirect_to secure_url, options.slice(*REDIRECT_OPTIONS) end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 86b5eb20d7..0c50894bce 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -18,13 +18,7 @@ module ActionController # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) if status.is_a?(Hash) - msg = status[:status] ? "The :status option" : "The implicit :ok status" - options, status = status, status.delete(:status) - - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #{msg} on `head` has been deprecated and will be removed in Rails 5.1. - Please pass the status as a separate parameter before the options, instead. - MSG + raise ArgumentError, "#{status.inspect} is not a valid value for `status`." end status ||= :ok @@ -43,7 +37,7 @@ module ActionController if include_content?(response_code) self.content_type = content_type || (Mime[formats.first] if formats) - self.response.charset = false + response.charset = false end true diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 476d081239..913a4b9a04 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -53,9 +53,8 @@ module ActionController include AbstractController::Helpers included do - class_attribute :helpers_path, :include_all_helpers - self.helpers_path ||= [] - self.include_all_helpers = true + class_attribute :helpers_path, default: [] + class_attribute :include_all_helpers, default: true end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index a335bf109e..d8bc895265 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -28,7 +28,7 @@ module ActionController # class ApplicationController < ActionController::Base # before_action :set_account, :authenticate # - # protected + # private # def set_account # @account = Account.find_by(url_name: request.subdomains.first) # end @@ -224,7 +224,7 @@ module ActionController # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead # of a plain-text password. - def expected_response(http_method, uri, credentials, password, password_is_ha1=true) + def expected_response(http_method, uri, credentials, password, password_is_ha1 = true) ha1 = password_is_ha1 ? password : ha1(credentials, password) ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":")) ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":")) @@ -246,7 +246,7 @@ module ActionController def decode_credentials(header) ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair| key, value = pair.split("=", 2) - [key.strip, value.to_s.gsub(/^"|"$/,"").delete('\'')] + [key.strip, value.to_s.gsub(/^"|"$/, "").delete('\'')] end] end @@ -314,7 +314,7 @@ module ActionController # Can be much shorter if the Stale directive is implemented. This would # allow a user to use new nonce without prompting the user again for their # username and password. - def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) + def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60) return false if value.nil? t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout @@ -363,7 +363,7 @@ module ActionController # class ApplicationController < ActionController::Base # before_action :set_account, :authenticate # - # protected + # private # def set_account # @account = Account.find_by(url_name: request.subdomains.first) # end @@ -445,7 +445,7 @@ module ActionController end end - # Parses the token and options out of the token authorization header. + # Parses the token and options out of the token Authorization header. # The value for the Authorization header is expected to have the prefix # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this: # Authorization: Token token="abc", nonce="def" diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 8615c16c6f..eeb27f99f4 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,14 +1,12 @@ -require "active_support/core_ext/string/strip" - module ActionController # Handles implicit rendering for a controller action that does not # explicitly respond with +render+, +respond_to+, +redirect+, or +head+. # - # For API controllers, the implicit response is always 204 No Content. + # For API controllers, the implicit response is always <tt>204 No Content</tt>. # # For all other controllers, we use these heuristics to decide whether to # render a template, raise an error for a missing template, or respond with - # 204 No Content: + # <tt>204 No Content</tt>: # # First, if we DO find a template, it's rendered. Template lookup accounts # for the action name, locales, format, variant, template handlers, and more @@ -25,7 +23,7 @@ module ActionController # <tt>ActionView::UnknownFormat</tt> with an explanation. # # Finally, if we DON'T find a template AND the request isn't a browser page - # load, then we implicitly respond with 204 No Content. + # load, then we implicitly respond with <tt>204 No Content</tt>. module ImplicitRender # :stopdoc: include BasicImplicitRender diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 2ede96c667..2485d27cec 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -3,7 +3,7 @@ require "abstract_controller/logger" module ActionController # Adds instrumentation to several ends in ActionController::Base. It also provides - # some hooks related with process_action, this allows an ORM like Active Record + # some hooks related with process_action. This allows an ORM like Active Record # and/or DataMapper to plug in ActionController and show related information. # # Check ActiveRecord::Railties::ControllerRuntime for an example. @@ -46,7 +46,7 @@ module ActionController render_output end - def send_file(path, options={}) + def send_file(path, options = {}) ActiveSupport::Notifications.instrument("send_file.action_controller", options.merge(path: path)) do super @@ -83,14 +83,14 @@ module ActionController # end # # :api: plugin - def cleanup_view_runtime #:nodoc: + def cleanup_view_runtime yield end # Every time after an action is processed, this method is invoked # with the payload, so you can add more information. # :api: plugin - def append_info_to_payload(payload) #:nodoc: + def append_info_to_payload(payload) payload[:view_runtime] = view_runtime end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 26a16104db..a607ee2309 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -239,15 +239,15 @@ module ActionController error = nil # This processes the action in a child thread. It lets us return the - # response code and headers back up the rack stack, and still process - # the body in parallel with sending data to the client + # response code and headers back up the Rack stack, and still process + # the body in parallel with sending data to the client. new_controller_thread { ActiveSupport::Dependencies.interlock.running do t2 = Thread.current # Since we're processing the view in a different thread, copy the # thread locals from the main thread to the child thread. :'( - locals.each { |k,v| t2[k] = v } + locals.each { |k, v| t2[k] = v } begin super(name) @@ -278,9 +278,9 @@ module ActionController raise error if error end - # Spawn a new thread to serve up the controller in. This is to get + # Spawn a new thread to serve up the controller in. This is to get # around the fact that Rack isn't based around IOs and we need to use - # a thread to stream data from the response bodies. Nobody should call + # a thread to stream data from the response bodies. Nobody should call # this method except in Rails internals. Seriously! def new_controller_thread # :nodoc: Thread.new { diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index f6aabcb102..96bd548268 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -181,8 +181,8 @@ module ActionController #:nodoc: # # request.variant = [:tablet, :phone] # - # which will work similarly to formats and MIME types negotiation. If there will be no - # +:tablet+ variant declared, +:phone+ variant will be picked: + # This will work similarly to formats and MIME types negotiation. If there + # is no +:tablet+ variant declared, the +:phone+ variant will be used: # # respond_to do |format| # format.html.none diff --git a/actionpack/lib/action_controller/metal/parameter_encoding.rb b/actionpack/lib/action_controller/metal/parameter_encoding.rb index c457fd0d06..ecc691619e 100644 --- a/actionpack/lib/action_controller/metal/parameter_encoding.rb +++ b/actionpack/lib/action_controller/metal/parameter_encoding.rb @@ -1,5 +1,5 @@ module ActionController - # Allows encoding to be specified per parameter per action. + # Specify binary encoding for parameters for a given action. module ParameterEncoding extend ActiveSupport::Concern @@ -13,17 +13,36 @@ module ActionController @_parameter_encodings = {} end - def encoding_for_param(action, param) # :nodoc: - if @_parameter_encodings[action.to_s] && @_parameter_encodings[action.to_s][param.to_s] - @_parameter_encodings[action.to_s][param.to_s] - else - super - end + def binary_params_for?(action) # :nodoc: + @_parameter_encodings[action.to_s] end - def parameter_encoding(action, param_name, encoding) - @_parameter_encodings[action.to_s] ||= {} - @_parameter_encodings[action.to_s][param_name.to_s] = encoding + # Specify that a given action's parameters should all be encoded as + # ASCII-8BIT (it "skips" the encoding default of UTF-8). + # + # For example, a controller would use it like this: + # + # class RepositoryController < ActionController::Base + # skip_parameter_encoding :show + # + # def show + # @repo = Repository.find_by_filesystem_path params[:file_path] + # + # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so + # # tag it as such + # @repo_name = params[:repo_name].force_encoding 'UTF-8' + # end + # + # def index + # @repositories = Repository.all + # end + # end + # + # The show action in the above controller would have all parameter values + # encoded as ASCII-8BIT. This is useful in the case where an application + # must handle data but encoding of the data is unknown, like file system data. + def skip_parameter_encoding(action) + @_parameter_encodings[action.to_s] = true end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 9d1b740025..68881b8402 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -105,7 +105,11 @@ module ActionController unless super || exclude if m.respond_to?(:attribute_names) && m.attribute_names.any? - self.include = m.attribute_names + if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty? + self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s) + else + self.include = m.attribute_names + end end end end @@ -135,7 +139,7 @@ module ActionController # # This method also does namespace lookup. Foo::Bar::UsersController will # try to find Foo::Bar::User, Foo::User and finally User. - def _default_wrap_model #:nodoc: + def _default_wrap_model return nil if klass.anonymous? model_name = klass.name.sub(/Controller$/, "").classify @@ -155,8 +159,7 @@ module ActionController end included do - class_attribute :_wrapper_options - self._wrapper_options = Options.from_hash(format: []) + class_attribute :_wrapper_options, default: Options.from_hash(format: []) end module ClassMethods @@ -205,7 +208,7 @@ module ActionController model = name_or_model_or_options end - opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options) + opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options) opts.model = model opts.klass = self @@ -213,7 +216,7 @@ module ActionController end # Sets the default wrapper key or model which will be used to determine - # wrapper key and attribute names. Will be called automatically when the + # wrapper key and attribute names. Called automatically when the # module is inherited. def inherited(klass) if klass._wrapper_options.format.any? @@ -225,7 +228,7 @@ module ActionController end end - # Performs parameters wrapping upon the request. Will be called automatically + # Performs parameters wrapping upon the request. Called automatically # by the metal call stack. def process_action(*args) if _wrapper_enabled? @@ -238,11 +241,11 @@ module ActionController wrapped_keys = request.request_parameters.keys wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) - # This will make the wrapped hash accessible from controller and view + # This will make the wrapped hash accessible from controller and view. request.parameters.merge! wrapped_hash request.request_parameters.merge! wrapped_hash - # This will display the wrapped hash in the log file + # This will display the wrapped hash in the log file. request.filtered_parameters.merge! wrapped_filtered_hash end super diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 2bd4296aff..fdfe82f96b 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -1,12 +1,4 @@ module ActionController - class RedirectBackError < AbstractController::Error #:nodoc: - DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].' - - def initialize(message = nil) - super(message || DEFAULT_MESSAGE) - end - end - module Redirecting extend ActiveSupport::Concern @@ -24,13 +16,13 @@ module ActionController # === Examples: # # redirect_to action: "show", id: 5 - # redirect_to post + # redirect_to @post # redirect_to "http://www.rubyonrails.org" # redirect_to "/images/screenshot.jpg" - # redirect_to articles_url + # redirect_to posts_url # redirect_to proc { edit_post_url(@post) } # - # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: + # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option: # # redirect_to post_url(@post), status: :found # redirect_to action: 'atom', status: :moved_permanently @@ -44,7 +36,7 @@ module ActionController # If you are using XHR requests other than GET or POST and redirecting after the # request then some browsers will follow the redirect using the original request # method. This may lead to undesirable behavior such as a double DELETE. To work - # around this you can return a <tt>303 See Other</tt> status code which will be + # around this you can return a <tt>303 See Other</tt> status code which will be # followed using a GET request. # # redirect_to posts_url, status: :see_other @@ -58,13 +50,16 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - def redirect_to(options = {}, response_status = {}) #:doc: + # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function. + # To terminate the execution of the function immediately after the +redirect_to+, use return. + # redirect_to post_url(@post) and return + def redirect_to(options = {}, response_status = {}) raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(request, options) - self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>" end # Redirects the browser to the page that issued the request (the referrer) @@ -77,11 +72,11 @@ module ActionController # is missing this header, the <tt>fallback_location</tt> will be used. # # redirect_back fallback_location: { action: "show", id: 5 } - # redirect_back fallback_location: post + # redirect_back fallback_location: @post # redirect_back fallback_location: "http://www.rubyonrails.org" - # redirect_back fallback_location: "/images/screenshot.jpg" - # redirect_back fallback_location: articles_url - # redirect_back fallback_location: proc { edit_post_url(@post) } + # redirect_back fallback_location: "/images/screenshot.jpg" + # redirect_back fallback_location: posts_url + # redirect_back fallback_location: proc { edit_post_url(@post) } # # All options that can be passed to <tt>redirect_to</tt> are accepted as # options and the behavior is identical. @@ -104,14 +99,6 @@ module ActionController options when String request.protocol + request.host_with_port + options - when :back - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - `redirect_to :back` is deprecated and will be removed from Rails 5.1. - Please use `redirect_back(fallback_location: fallback_location)` where - `fallback_location` represents the location to use if the request has - no HTTP referer information. - MESSAGE - request.headers["Referer"] || raise(RedirectBackError) when Proc _compute_redirect_to_location request, options.call else diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 15377ddcb9..23c21b0501 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -26,8 +26,7 @@ module ActionController RENDERERS = Set.new included do - class_attribute :_renderers - self._renderers = Set.new.freeze + class_attribute :_renderers, default: Set.new.freeze end # Used in <tt>ActionController::Base</tt> @@ -71,8 +70,6 @@ module ActionController # format.csv { render csv: @csvable, filename: @csvable.name } # end # end - # To use renderers and their mime types in more concise ways, see - # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> def self.add(key, &block) define_method(_render_with_renderer_method_name(key), &block) RENDERERS << key.to_sym @@ -106,7 +103,7 @@ module ActionController # # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, - # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # and <tt>ActionController::Renderers</tt>, and have at least one renderer. # # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, # you may specify which renderers to include by passing the renderer name or names to diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index f8f91ed41c..67f207afc2 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -4,7 +4,7 @@ module ActionController module Rendering extend ActiveSupport::Concern - RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html] module ClassMethods # Documentation at ActionController::Renderer#render @@ -36,7 +36,7 @@ module ActionController super end - # Overwrite render_to_string because body can now be set to a rack body. + # Overwrite render_to_string because body can now be set to a Rack body. def render_to_string(*) result = super if result.respond_to?(:each) @@ -54,6 +54,12 @@ module ActionController private + def _process_variant(options) + if defined?(request) && !request.nil? && request.variant.present? + options[:variant] = request.variant + end + end + def _render_in_priorities(options) RENDER_FORMATS_IN_PRIORITY.each do |format| return options[format] if options.key?(format) @@ -67,42 +73,26 @@ module ActionController end def _set_rendered_content_type(format) - unless response.content_type + if format && !response.content_type self.content_type = format.to_s end end # Normalize arguments by catching blocks and setting them on :update. - def _normalize_args(action=nil, options={}, &blk) #:nodoc: + def _normalize_args(action = nil, options = {}, &blk) options = super options[:update] = blk if block_given? options end # Normalize both text and status options. - def _normalize_options(options) #:nodoc: + def _normalize_options(options) _normalize_text(options) - if options[:text] - ActiveSupport::Deprecation.warn <<-WARNING.squish - `render :text` is deprecated because it does not actually render a - `text/plain` response. Switch to `render plain: 'plain text'` to - render as `text/plain`, `render html: '<strong>HTML</strong>'` to - render as `text/html`, or `render body: 'raw'` to match the deprecated - behavior and render with the default Content-Type, which is - `text/html`. - WARNING - end - if options[:html] options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) - ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.") - options[:body] = nil - end - if options[:status] options[:status] = Rack::Utils.status_code(options[:status]) end @@ -119,12 +109,12 @@ module ActionController end # Process controller specific options, as status, content-type and location. - def _process_options(options) #:nodoc: + def _process_options(options) status, content_type, location = options.values_at(:status, :content_type, :location) self.status = status if status self.content_type = content_type if content_type - self.headers["Location"] = url_for(location) if location + headers["Location"] = url_for(location) if location super end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 3d3c121280..5051c02a62 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -152,7 +152,7 @@ module ActionController #:nodoc: request.cookie_jar = NullCookieJar.build(request, {}) end - protected + private class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: def initialize(req) @@ -197,7 +197,7 @@ module ActionController #:nodoc: end end - protected + private # 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 @@ -208,18 +208,22 @@ module ActionController #:nodoc: # enabled on an action, this before_action flags its after_action to # verify that JavaScript responses are for XHR requests, ensuring they # follow the browser's same-origin policy. - def verify_authenticity_token + def verify_authenticity_token # :doc: mark_for_same_origin_verification! if !verified_request? if logger && log_warning_on_csrf_failure - logger.warn "Can't verify CSRF token authenticity." + if valid_request_origin? + logger.warn "Can't verify CSRF token authenticity." + else + logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})" + end end handle_unverified_request end end - def handle_unverified_request + def handle_unverified_request # :doc: forgery_protection_strategy.new(self).handle_unverified_request end @@ -233,7 +237,7 @@ module ActionController #:nodoc: # 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 + def verify_same_origin_request # :doc: if marked_for_same_origin_verification? && non_xhr_javascript_response? if logger && log_warning_on_csrf_failure logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING @@ -243,18 +247,18 @@ module ActionController #:nodoc: end # GET requests are checked for cross-origin JavaScript after rendering. - def mark_for_same_origin_verification! + def mark_for_same_origin_verification! # :doc: @marked_for_same_origin_verification = request.get? end # 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? + def marked_for_same_origin_verification? # :doc: @marked_for_same_origin_verification ||= false end # Check for cross-origin JavaScript responses. - def non_xhr_javascript_response? + def non_xhr_javascript_response? # :doc: content_type =~ %r(\Atext/javascript) && !request.xhr? end @@ -262,23 +266,23 @@ module ActionController #:nodoc: # Returns true or false if a request is verified. Checks: # - # * Is it a GET or HEAD request? Gets should be safe and idempotent + # * Is it a GET or HEAD request? GETs should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? - # * Does the X-CSRF-Token header match the form_authenticity_token - def verified_request? + # * Does the X-CSRF-Token header match the form_authenticity_token? + def verified_request? # :doc: !protect_against_forgery? || request.get? || request.head? || (valid_request_origin? && any_authenticity_token_valid?) end # Checks if any of the authenticity tokens from the request are valid. - def any_authenticity_token_valid? + def any_authenticity_token_valid? # :doc: request_authenticity_tokens.any? do |token| valid_authenticity_token?(session, token) end end # Possible authenticity tokens sent in the request. - def request_authenticity_tokens + def request_authenticity_tokens # :doc: [form_authenticity_param, request.x_csrf_token] end @@ -290,7 +294,7 @@ module ActionController #:nodoc: # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session, form_options: {}) + def masked_authenticity_token(session, form_options: {}) # :doc: action, method = form_options.values_at(:action, :method) raw_token = if per_form_csrf_tokens && action && method @@ -309,7 +313,7 @@ module ActionController #:nodoc: # Checks the client's masked token to see if it matches the # session token. Essentially the inverse of # +masked_authenticity_token+. - def valid_authenticity_token?(session, encoded_masked_token) + def valid_authenticity_token?(session, encoded_masked_token) # :doc: if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) return false end @@ -327,7 +331,7 @@ module ActionController #:nodoc: if masked_token.length == AUTHENTICITY_TOKEN_LENGTH # This is actually an unmasked token. This is expected if # you have just upgraded to masked tokens, but should stop - # happening shortly after installing this gem + # happening shortly after installing this gem. compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 @@ -336,23 +340,23 @@ module ActionController #:nodoc: compare_with_real_token(csrf_token, session) || valid_per_form_csrf_token?(csrf_token, session) else - false # Token is malformed + false # Token is malformed. end end - def unmask_token(masked_token) + def unmask_token(masked_token) # :doc: # Split the token into the one-time pad and the encrypted - # value and decrypt it + # value and decrypt it. one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] xor_byte_strings(one_time_pad, encrypted_csrf_token) end - def compare_with_real_token(token, session) + def compare_with_real_token(token, session) # :doc: ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) end - def valid_per_form_csrf_token?(token, session) + def valid_per_form_csrf_token?(token, session) # :doc: if per_form_csrf_tokens correct_token = per_form_csrf_token( session, @@ -366,12 +370,12 @@ module ActionController #:nodoc: end end - def real_csrf_token(session) + def real_csrf_token(session) # :doc: session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end - def per_form_csrf_token(session, action_path, method) + def per_form_csrf_token(session, action_path, method) # :doc: OpenSSL::HMAC.digest( OpenSSL::Digest::SHA256.new, real_csrf_token(session), @@ -379,25 +383,25 @@ module ActionController #:nodoc: ) end - def xor_byte_strings(s1, s2) + def xor_byte_strings(s1, s2) # :doc: s2_bytes = s2.bytes s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 } s2_bytes.pack("C*") end # The form's authenticity parameter. Override to provide your own. - def form_authenticity_param + def form_authenticity_param # :doc: params[request_forgery_protection_token] end # Checks if the controller allows forgery protection. - def protect_against_forgery? + def protect_against_forgery? # :doc: allow_forgery_protection end # Checks if the request originated from the same origin by looking at the # Origin header. - def valid_request_origin? + def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. request.origin.nil? || request.origin == request.base_url @@ -406,7 +410,7 @@ module ActionController #:nodoc: end end - def normalize_action_path(action_path) + def normalize_action_path(action_path) # :doc: uri = URI.parse(action_path) uri.path.chomp("/") end diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index 2d99e4045b..25757938f5 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -10,7 +10,7 @@ module ActionController #:nodoc: # exceptions must be shown. This method is only called when # consider_all_requests_local is false. By default, it returns # false, but someone may set it to `request.local?` so local - # requests in production still shows the detailed exception pages. + # requests in production still show the detailed exception pages. def show_detailed_exceptions? false end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 481f19f1ef..58cf60ad2a 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -3,7 +3,7 @@ require "rack/chunked" module ActionController #:nodoc: # Allows views to be streamed back to the client as they are rendered. # - # The default way Rails renders views is by first rendering the template + # By default, Rails renders views by first rendering the template # and then the layout. The response is sent to the client after the whole # template is rendered, all queries are made, and the layout is processed. # @@ -193,10 +193,10 @@ module ActionController #:nodoc: module Streaming extend ActiveSupport::Concern - protected + private # Set proper cache control and transfer encoding when streaming - def _process_options(options) #:nodoc: + def _process_options(options) super if options[:stream] if request.version == "HTTP/1.0" @@ -210,7 +210,7 @@ module ActionController #:nodoc: end # Call render_body if we are streaming instead of usual +render+. - def _render_template(options) #:nodoc: + def _render_template(options) if options.delete(:stream) Rack::Chunked::Body.new view_renderer.render_body(view_context, options) else diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 387c2aa0b9..20330b5091 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -43,6 +43,18 @@ module ActionController end end + # Raised when a Parameters instance is not marked as permitted and + # an operation to transform it to hash is called. + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.to_h + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash + class UnfilteredParameters < ArgumentError + def initialize # :nodoc: + super("unable to convert unpermitted parameters to hash") + end + end + # == Action Controller \Parameters # # Allows you to choose which attributes should be whitelisted for mass updating @@ -53,9 +65,9 @@ module ActionController # # params = ActionController::Parameters.new({ # person: { - # name: 'Francesco', + # name: "Francesco", # age: 22, - # role: 'admin' + # role: "admin" # } # }) # @@ -71,8 +83,8 @@ module ActionController # * +permit_all_parameters+ - If it's +true+, all the parameters will be # permitted by default. The default is +false+. # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters - # that are not explicitly permitted are found. The values can be <tt>:log</tt> to - # write a message on the logger or <tt>:raise</tt> to raise + # that are not explicitly permitted are found. The values can be +false+ to just filter them + # out, <tt>:log</tt> to additionally write a message on the logger, or <tt>:raise</tt> to raise # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt> # in test and development environments, +false+ otherwise. # @@ -103,7 +115,7 @@ module ActionController # You can fetch values of <tt>ActionController::Parameters</tt> using either # <tt>:key</tt> or <tt>"key"</tt>. # - # params = ActionController::Parameters.new(key: 'value') + # params = ActionController::Parameters.new(key: "value") # params[:key] # => "value" # params["key"] # => "value" class Parameters @@ -112,6 +124,77 @@ module ActionController cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + ## + # :method: as_json + # + # :call-seq: + # as_json(options=nil) + # + # Returns a hash that can be used as the JSON representation for the parameters. + + ## + # :method: empty? + # + # :call-seq: + # empty?() + # + # Returns true if the parameters have no key/value pairs. + + ## + # :method: has_key? + # + # :call-seq: + # has_key?(key) + # + # Returns true if the given key is present in the parameters. + + ## + # :method: has_value? + # + # :call-seq: + # has_value?(value) + # + # Returns true if the given value is present for some key in the parameters. + + ## + # :method: include? + # + # :call-seq: + # include?(key) + # + # Returns true if the given key is present in the parameters. + + ## + # :method: key? + # + # :call-seq: + # key?(key) + # + # Returns true if the given key is present in the parameters. + + ## + # :method: keys + # + # :call-seq: + # keys() + # + # Returns a new array of the keys of the parameters. + + ## + # :method: value? + # + # :call-seq: + # value?(value) + # + # Returns true if the given value is present for some key in the parameters. + + ## + # :method: values + # + # :call-seq: + # values() + # + # Returns a new array of the values of the parameters. delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, :as_json, to: :@parameters @@ -132,13 +215,13 @@ module ActionController # class Person < ActiveRecord::Base # end # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # # ActionController::Parameters.permit_all_parameters = true # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> def initialize(parameters = {}) @@ -150,29 +233,21 @@ module ActionController # permitted flag. def ==(other) if other.respond_to?(:permitted?) - self.permitted? == other.permitted? && self.parameters == other.parameters - elsif other.is_a?(Hash) - ActiveSupport::Deprecation.warn <<-WARNING.squish - Comparing equality between `ActionController::Parameters` and a - `Hash` is deprecated and will be removed in Rails 5.1. Please only do - comparisons between instances of `ActionController::Parameters`. If - you need to compare to a hash, first convert it using - `ActionController::Parameters#new`. - WARNING - @parameters == other.with_indifferent_access + permitted? == other.permitted? && parameters == other.parameters else @parameters == other end end # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt> - # representation of this parameter with all unpermitted keys removed. + # representation of the parameters with all unpermitted keys removed. # # params = ActionController::Parameters.new({ - # name: 'Senjougahara Hitagi', - # oddity: 'Heavy stone crab' + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" # }) - # params.to_h # => {} + # params.to_h + # # => ActionController::UnfilteredParameters: unable to convert unfiltered parameters to hash # # safe_params = params.permit(:name) # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} @@ -180,17 +255,61 @@ module ActionController if permitted? convert_parameters_to_hashes(@parameters, :to_h) else - slice(*self.class.always_permitted_parameters).permit!.to_h + raise UnfilteredParameters end end + # Returns a safe <tt>Hash</tt> representation of the parameters + # with all unpermitted keys removed. + # + # params = ActionController::Parameters.new({ + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" + # }) + # params.to_hash + # # => ActionController::UnfilteredParameters: unable to convert unfiltered parameters to hash + # + # safe_params = params.permit(:name) + # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"} + def to_hash + to_h.to_hash + end + + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # params = ActionController::Parameters.new({ + # name: "David", + # nationality: "Danish" + # }) + # params.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # params = ActionController::Parameters.new({ + # name: "David", + # nationality: "Danish" + # }) + # params.to_query("user") + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(*args) + to_h.to_query(*args) + end + alias_method :to_param, :to_query + # Returns an unsafe, unfiltered - # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this - # parameter. + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the + # parameters. # # params = ActionController::Parameters.new({ - # name: 'Senjougahara Hitagi', - # oddity: 'Heavy stone crab' + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" # }) # params.to_unsafe_h # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"} @@ -200,7 +319,7 @@ module ActionController alias_method :to_unsafe_hash, :to_unsafe_h # Convert all hashes in values into parameters, then yield each pair in - # the same way as <tt>Hash#each_pair</tt> + # 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) @@ -235,7 +354,7 @@ module ActionController # class Person < ActiveRecord::Base # end # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # params.permit! @@ -257,7 +376,7 @@ module ActionController # When passed a single key, if it exists and its associated value is # either present or the singleton +false+, returns said value: # - # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) + # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person) # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # # Otherwise raises <tt>ActionController::ParameterMissing</tt>: @@ -290,7 +409,7 @@ module ActionController # Technically this method can be used to fetch terminal values: # # # CAREFUL - # params = ActionController::Parameters.new(person: { name: 'Finn' }) + # params = ActionController::Parameters.new(person: { name: "Finn" }) # name = params.require(:person).require(:name) # CAREFUL # # but take into account that at some point those ones have to be permitted: @@ -320,7 +439,7 @@ module ActionController # for the object to +true+. This is useful for limiting which attributes # should be allowed for mass updating. # - # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' }) + # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" }) # permitted = params.require(:user).permit(:name, :age) # permitted.permitted? # => true # permitted.has_key?(:name) # => true @@ -340,18 +459,27 @@ module ActionController # You may declare that the parameter should be an array of permitted scalars # by mapping it to an empty array: # - # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) + # params = ActionController::Parameters.new(tags: ["rails", "parameters"]) # params.permit(tags: []) # + # Sometimes it is not possible or convenient to declare the valid keys of + # a hash parameter or its internal structure. Just map to an empty hash: + # + # params.permit(preferences: {}) + # + # Be careful because this opens the door to arbitrary input. In this + # case, +permit+ ensures values in the returned structure are permitted + # scalars and filters out anything else. + # # You can also use +permit+ on nested parameters, like: # # params = ActionController::Parameters.new({ # person: { - # name: 'Francesco', + # name: "Francesco", # age: 22, # pets: [{ - # name: 'Purplish', - # category: 'dogs' + # name: "Purplish", + # category: "dogs" # }] # } # }) @@ -370,8 +498,8 @@ module ActionController # params = ActionController::Parameters.new({ # person: { # contact: { - # email: 'none@test.com', - # phone: '555-1234' + # email: "none@test.com", + # phone: "555-1234" # } # } # }) @@ -391,7 +519,7 @@ module ActionController case filter when Symbol, String permitted_scalar_filter(params, filter) - when Hash then + when Hash hash_filter(params, filter) end end @@ -404,7 +532,7 @@ module ActionController # Returns a parameter for the given +key+. If not found, # returns +nil+. # - # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params = ActionController::Parameters.new(person: { name: "Francesco" }) # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params[:none] # => nil def [](key) @@ -423,11 +551,11 @@ module ActionController # if more arguments are given, then that will be returned; if a block # is given, then that will be run and its result returned. # - # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params = ActionController::Parameters.new(person: { name: "Francesco" }) # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none - # params.fetch(:none, 'Francesco') # => "Francesco" - # params.fetch(:none) { 'Francesco' } # => "Francesco" + # params.fetch(:none, "Francesco") # => "Francesco" + # params.fetch(:none) { "Francesco" } # => "Francesco" def fetch(key, *args) convert_value_to_parameters( @parameters.fetch(key) { @@ -538,8 +666,8 @@ module ActionController # to key. If the key is not found, returns the default value. If the # optional code block is given and the key is not found, pass in the key # and return the result of block. - def delete(key) - convert_value_to_parameters(@parameters.delete(key)) + def delete(key, &block) + convert_value_to_parameters(@parameters.delete(key, &block)) end # Returns a new instance of <tt>ActionController::Parameters</tt> with only @@ -548,7 +676,7 @@ module ActionController new_instance_with_inherited_permitted_status(@parameters.select(&block)) end - # Equivalent to Hash#keep_if, but returns nil if no changes were made. + # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made. def select!(&block) @parameters.select!(&block) self @@ -575,13 +703,37 @@ module ActionController end # Returns a new <tt>ActionController::Parameters</tt> with all keys from - # +other_hash+ merges into current hash. + # +other_hash+ merged into current hash. def merge(other_hash) new_instance_with_inherited_permitted_status( @parameters.merge(other_hash.to_h) ) end + # Returns current <tt>ActionController::Parameters</tt> instance with + # +other_hash+ merged into current hash. + def merge!(other_hash) + @parameters.merge!(other_hash.to_h) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> with all keys from + # current hash merged into +other_hash+. + def reverse_merge(other_hash) + new_instance_with_inherited_permitted_status( + other_hash.to_h.merge(@parameters) + ) + end + alias_method :with_defaults, :reverse_merge + + # Returns current <tt>ActionController::Parameters</tt> instance with + # current hash merged into +other_hash+. + def reverse_merge!(other_hash) + @parameters.merge!(other_hash.to_h) { |key, left, right| left } + self + end + alias_method :with_defaults!, :reverse_merge! + # This is required by ActiveModel attribute assignment, so that user can # pass +Parameters+ to a mass assignment methods in a model. It should not # matter as we are using +HashWithIndifferentAccess+ internally. @@ -620,25 +772,10 @@ module ActionController end end - # Undefine `to_param` such that it gets caught in the `method_missing` - # deprecation cycle below. - undef_method :to_param - - def method_missing(method_sym, *args, &block) - if @parameters.respond_to?(method_sym) - message = <<-DEPRECATE.squish - Method #{method_sym} is deprecated and will be removed in Rails 5.1, - as `ActionController::Parameters` no longer inherits from - hash. Using this deprecated behavior exposes potential security - problems. If you continue to use this method you may be creating - a security vulnerability in your app that can be exploited. Instead, - consider using one of these documented methods which are not - deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html - DEPRECATE - ActiveSupport::Deprecation.warn(message) - @parameters.public_send(method_sym, *args, &block) - else - super + # Returns duplicate of object including all parameters. + def deep_dup + self.class.new(@parameters.deep_dup).tap do |duplicate| + duplicate.permitted = @permitted end end @@ -702,7 +839,7 @@ module ActionController when Parameters if object.fields_for_style? hash = object.class.new - object.each { |k,v| hash[k] = yield v } + object.each { |k, v| hash[k] = yield v } hash else yield object @@ -781,6 +918,7 @@ module ActionController end EMPTY_ARRAY = [] + EMPTY_HASH = {} def hash_filter(params, filter) filter = filter.with_indifferent_access @@ -794,6 +932,11 @@ module ActionController array_of_permitted_scalars?(self[key]) do |val| params[key] = val end + elsif filter[key] == EMPTY_HASH + # Declaration { preferences: {} }. + if value.is_a?(Parameters) + params[key] = permit_any_in_parameters(value) + end elsif non_scalar?(value) # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| @@ -803,6 +946,38 @@ module ActionController end end + def permit_any_in_parameters(params) + self.class.new.tap do |sanitized| + params.each do |key, value| + case value + when ->(v) { permitted_scalar?(v) } + sanitized[key] = value + when Array + sanitized[key] = permit_any_in_array(value) + when Parameters + sanitized[key] = permit_any_in_parameters(value) + else + # Filter this one out. + end + end + end + end + + def permit_any_in_array(array) + [].tap do |sanitized| + array.each do |element| + case element + when ->(e) { permitted_scalar?(e) } + sanitized << element + when Parameters + sanitized << permit_any_in_parameters(element) + else + # Filter this one out. + end + end + end + end + def initialize_copy(source) super @parameters = @parameters.dup @@ -817,7 +992,7 @@ module ActionController # whitelisted. # # In addition, parameters can be marked as required and flow through a - # predefined raise/rescue flow to end up as a 400 Bad Request with no + # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no # effort. # # class PeopleController < ActionController::Base @@ -830,7 +1005,7 @@ module ActionController # end # # # This will pass with flying colors as long as there's a person key in the - # # parameters, otherwise it'll raise an ActionController::MissingParameter + # # parameters, otherwise it'll raise an ActionController::ParameterMissing # # exception, which will get caught by ActionController::Base and turned # # into a 400 Bad Request reply. # def update @@ -841,7 +1016,7 @@ module ActionController # # private # # Using a private method to encapsulate the permissible parameters is - # # just a good pattern since you'll be able to reuse the same permit + # # a good pattern since you'll be able to reuse the same permit # # list between create and update. Also, you can specialize this method # # with per-user checking of permissible attributes. # def person_params diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 9f3cc099d6..21ed5b4ec8 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -3,7 +3,7 @@ module ActionController # the <tt>_routes</tt> method. Otherwise, an exception will be raised. # # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define - # url options like the +host+. In order to do so, this module requires the host class + # URL options like the +host+. In order to do so, this module requires the host class # to implement +env+ which needs to be Rack-compatible and +request+ # which is either an instance of +ActionDispatch::Request+ or an object # that responds to the +host+, +optional_port+, +protocol+ and diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 6513a556ee..fadfc8de60 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -42,7 +42,7 @@ module ActionController options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first - # Ensure readers methods get compiled + # Ensure readers methods get compiled. options.asset_host ||= app.config.asset_host options.relative_url_root ||= app.config.relative_url_root @@ -51,7 +51,7 @@ module ActionController extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) extend ::ActionController::Railties::Helpers - options.each do |k,v| + options.each do |k, v| k = "#{k}=" if respond_to?(k) send(k, v) diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index 0739f16965..cbb719d8b2 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -5,7 +5,7 @@ module ActionController # without requirement of being in controller actions. # # You get a concrete renderer class by invoking ActionController::Base#renderer. - # For example, + # For example: # # ApplicationController.renderer # @@ -18,7 +18,7 @@ module ActionController # ApplicationController.render template: '...' # # #render allows you to use the same options that you can use when rendering in a controller. - # For example, + # For example: # # FooController.render :action, locals: { ... }, assigns: { ... } # @@ -56,11 +56,12 @@ module ActionController # Create a new renderer for the same controller but with new defaults. def with_defaults(defaults) - self.class.new controller, env, self.defaults.merge(defaults) + self.class.new controller, @env, self.defaults.merge(defaults) end # Accepts a custom Rack environment to render templates in. - # It will be merged with ActionController::Renderer.defaults + # It will be merged with the default Rack environment defined by + # +ActionController::Renderer::DEFAULTS+. def initialize(controller, env, defaults) @controller = controller @defaults = defaults @@ -83,7 +84,8 @@ module ActionController private def normalize_keys(env) new_env = {} - env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) } + env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) } + new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http" new_env end @@ -102,7 +104,9 @@ module ActionController method: ->(v) { v.upcase }, } - def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end + def rack_key_for(key) + RACK_KEY_TRANSLATION.fetch(key, key.to_s) + end def rack_value_for(key, value) RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 09f2a79d85..bc42d50205 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -3,6 +3,7 @@ require "active_support/core_ext/hash/conversions" require "active_support/core_ext/object/to_query" require "active_support/core_ext/module/anonymous" require "active_support/core_ext/hash/keys" +require "active_support/testing/constant_lookup" require "action_controller/template_assertions" require "rails-dom-testing" @@ -12,10 +13,10 @@ module ActionController end module Live - # Disable controller / rendering threads in tests. User tests can access + # Disable controller / rendering threads in tests. User tests can access # the database on the main thread, so they could open a txn, then the # controller thread will open a new connection and try to access data - # that's only visible to the main thread's txn. This is the problem in #23483 + # that's only visible to the main thread's txn. This is the problem in #23483. remove_method :new_controller_thread def new_controller_thread # :nodoc: yield @@ -34,7 +35,7 @@ module ActionController attr_reader :controller_class - # Create a new test request with default `env` values + # Create a new test request with default `env` values. def self.create(controller_class) env = {} env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application @@ -112,8 +113,9 @@ module ActionController end end - set_header "CONTENT_LENGTH", data.length.to_s - set_header "rack.input", StringIO.new(data) + data_stream = StringIO.new(data) + set_header "CONTENT_LENGTH", data_stream.length.to_s + set_header "rack.input", data_stream end fetch_header("PATH_INFO") do |k| @@ -129,7 +131,7 @@ module ActionController include Rack::Test::Utils def should_multipart?(params) - # FIXME: lifted from Rack-Test. We should push this separation upstream + # FIXME: lifted from Rack-Test. We should push this separation upstream. multipart = false query = lambda { |value| case value @@ -298,7 +300,7 @@ module ActionController # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" # assert flash.empty? # makes sure that there's nothing in the flash # - # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>. + # On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>. # # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another # action call which can then be asserted against. @@ -352,7 +354,7 @@ module ActionController end def controller_class - if current_controller_class = self._controller_class + if current_controller_class = _controller_class current_controller_class else self.controller_class = determine_default_controller_class(name) @@ -386,57 +388,42 @@ module ActionController # # Note that the request method is not verified. The different methods are # available to make the tests more expressive. - def get(action, *args) - res = process_with_kwargs("GET", action, *args) + def get(action, **args) + res = process(action, method: "GET", **args) cookies.update res.cookies res end # Simulate a POST request with the given parameters and set/volley the response. # See +get+ for more details. - def post(action, *args) - process_with_kwargs("POST", action, *args) + def post(action, **args) + process(action, method: "POST", **args) end # Simulate a PATCH request with the given parameters and set/volley the response. # See +get+ for more details. - def patch(action, *args) - process_with_kwargs("PATCH", action, *args) + def patch(action, **args) + process(action, method: "PATCH", **args) end # Simulate a PUT request with the given parameters and set/volley the response. # See +get+ for more details. - def put(action, *args) - process_with_kwargs("PUT", action, *args) + def put(action, **args) + process(action, method: "PUT", **args) end # Simulate a DELETE request with the given parameters and set/volley the response. # See +get+ for more details. - def delete(action, *args) - process_with_kwargs("DELETE", action, *args) + def delete(action, **args) + process(action, method: "DELETE", **args) end # Simulate a HEAD request with the given parameters and set/volley the response. # See +get+ for more details. - def head(action, *args) - process_with_kwargs("HEAD", action, *args) + def head(action, **args) + process(action, method: "HEAD", **args) end - def xml_http_request(*args) - ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - xhr and xml_http_request methods are deprecated in favor of - `get :index, xhr: true` and `post :create, xhr: true` - MSG - - @request.env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" - @request.env["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ") - __send__(*args).tap do - @request.env.delete "HTTP_X_REQUESTED_WITH" - @request.env.delete "HTTP_ACCEPT" - end - end - alias xhr :xml_http_request - # Simulate an HTTP request to +action+ by specifying request method, # parameters and set/volley the response. # @@ -467,40 +454,14 @@ module ActionController # respectively which will make tests more expressive. # # Note that the request method is not verified. - def process(action, *args) + def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil) check_required_ivars - if kwarg_request?(args) - parameters, session, body, flash, http_method, format, xhr, as = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr, :as) - else - http_method, parameters, session, flash = args - format = nil - - if parameters.is_a?(String) && http_method != "HEAD" - body = parameters - parameters = nil - end - - if parameters || session || flash - non_kwarg_request_warning - end - end - if body @request.set_header "RAW_POST_DATA", body end - if http_method - http_method = http_method.to_s.upcase - else - http_method = "GET" - end - - parameters ||= {} - - if format - parameters[:format] = format - end + http_method = method.to_s.upcase @html_document = nil @@ -521,7 +482,11 @@ module ActionController format ||= as end - parameters = parameters.symbolize_keys + parameters = params.symbolize_keys + + if format + parameters[:format] = format + end generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) generated_path = generated_path(generated_extras) @@ -550,8 +515,6 @@ module ActionController @request = @controller.request @response = @controller.response - @request.delete_header "HTTP_COOKIE" - if @request.have_cookie_jar? unless @request.cookie_jar.committed? @request.cookie_jar.write(@response) @@ -641,38 +604,6 @@ module ActionController env end - def process_with_kwargs(http_method, action, *args) - if kwarg_request?(args) - args.first.merge!(method: http_method) - process(action, *args) - else - non_kwarg_request_warning if args.any? - - args = args.unshift(http_method) - process(action, *args) - end - end - - REQUEST_KWARGS = %i(params session flash method body xhr) - def kwarg_request?(args) - args[0].respond_to?(:keys) && ( - (args[0].key?(:format) && args[0].keys.size == 1) || - args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } - ) - end - - def non_kwarg_request_warning - ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - ActionController::TestCase HTTP request methods will accept only - keyword arguments in future Rails versions. - - Examples: - - get :show, params: { id: 1 }, session: { user_id: 1 } - process :update, method: :post, params: { id: 1 } - MSG - end - def document_root_element html_document.root end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 60cee6acbd..303790e96d 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -54,7 +54,6 @@ module ActionDispatch autoload :ExceptionWrapper autoload :Executor autoload :Flash - autoload :ParamsParser autoload :PublicExceptions autoload :Reloader autoload :RemoteIp @@ -98,6 +97,8 @@ module ActionDispatch autoload :TestResponse autoload :AssertionResponse end + + autoload :SystemTestCase, "action_dispatch/system_test_case" end autoload :Mime, "action_dispatch/http/mime_type" diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index e5874a39f6..e584b84d92 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -51,28 +51,28 @@ module ActionDispatch @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}" end - protected + private - def parameter_filter + def parameter_filter # :doc: parameter_filter_for fetch_header("action_dispatch.parameter_filter") { return NULL_PARAM_FILTER } end - def env_filter + def env_filter # :doc: user_key = fetch_header("action_dispatch.parameter_filter") { return NULL_ENV_FILTER } parameter_filter_for(Array(user_key) + ENV_MATCH) end - def parameter_filter_for(filters) + def parameter_filter_for(filters) # :doc: ParameterFilter.new(filters) end KV_RE = "[^&;=]+" PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})} - def filtered_query_string + def filtered_query_string # :doc: query_string.gsub(PAIR_RE) do |_| parameter_filter.filter([[$1, $2]]).first.join("=") end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index d0c9413efa..19f89edbc1 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -29,8 +29,8 @@ module ActionDispatch content_mime_type && content_mime_type.to_s end - def has_content_type? - has_header? "CONTENT_TYPE" + def has_content_type? # :nodoc: + get_header "CONTENT_TYPE" end # Returns the accepted MIME type for the request. @@ -135,9 +135,7 @@ module ActionDispatch } end - # Receives an array of mimes and return the first user sent mime that - # matches the order array. - # + # Returns the first MIME type that matches the provided array of MIME types. def negotiate_mime(order) formats.each do |priority| if priority == Mime::ALL @@ -150,20 +148,20 @@ module ActionDispatch order.include?(Mime::ALL) ? format : nil end - protected + private BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ - def valid_accept_header + def valid_accept_header # :doc: (xhr? && (accept.present? || content_mime_type)) || (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS) end - def use_accept_header + def use_accept_header # :doc: !self.class.ignore_accept_header end - def format_from_path_extension + def format_from_path_extension # :doc: path = get_header("action_dispatch.original_path") || get_header("PATH_INFO") if match = path && path.match(/\.(\w+)\z/) Mime[match.captures.first] diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index b9121a577c..74a3afab44 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,7 +1,6 @@ # -*- frozen-string-literal: true -*- require "singleton" -require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/string/starts_ends_with" module Mime @@ -45,35 +44,9 @@ module Mime return type if type.is_a?(Type) EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } end - - def const_missing(sym) - ext = sym.downcase - if Mime[ext] - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Accessing mime types via constants is deprecated. - Please change `Mime::#{sym}` to `Mime[:#{ext}]`. - MSG - Mime[ext] - else - super - end - end - - def const_defined?(sym, inherit = true) - ext = sym.downcase - if Mime[ext] - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Accessing mime types via constants is deprecated. - Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`. - MSG - true - else - super - end - end end - # Encapsulates the notion of a mime type. Can be used at render time, for example, with: + # Encapsulates the notion of a MIME type. Can be used at render time, for example, with: # # class PostsController < ActionController::Base # def show @@ -91,7 +64,7 @@ module Mime @register_callbacks = [] - # A simple helper class used in parsing the accept header + # A simple helper class used in parsing the accept header. class AcceptItem #:nodoc: attr_accessor :index, :name, :q alias :to_s :name @@ -99,7 +72,7 @@ module Mime def initialize(index, name, q = nil) @index = index @name = name - q ||= 0.0 if @name == "*/*".freeze # default wildcard match to end of list + q ||= 0.0 if @name == "*/*".freeze # Default wildcard match to end of list. @q = ((q || 1.0).to_f * 100).to_i end @@ -117,22 +90,22 @@ module Mime text_xml_idx = find_item_by_name list, "text/xml" app_xml_idx = find_item_by_name list, Mime[:xml].to_s - # Take care of the broken text/xml entry by renaming or deleting it + # Take care of the broken text/xml entry by renaming or deleting it. if text_xml_idx && app_xml_idx app_xml = list[app_xml_idx] text_xml = list[text_xml_idx] - app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two - if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list + app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two. + if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list. list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx end - list.delete_at(text_xml_idx) # delete text_xml from the list + list.delete_at(text_xml_idx) # Delete text_xml from the list. elsif text_xml_idx list[text_xml_idx].name = Mime[:xml].to_s end - # Look for more specific XML-based types and sort them ahead of app/xml + # Look for more specific XML-based types and sort them ahead of app/xml. if app_xml_idx app_xml = list[app_xml_idx] idx = app_xml_idx @@ -174,7 +147,7 @@ module Mime EXTENSION_LOOKUP[extension.to_s] end - # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for + # Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for # rendering different HTML versions depending on the user agent, like an iPhone. def register_alias(string, symbol, extension_synonyms = []) register(string, symbol, [], extension_synonyms, true) @@ -304,6 +277,8 @@ module Mime def all?; false; end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :string, :synonyms @@ -321,8 +296,8 @@ module Mime end end - def respond_to_missing?(method, include_private = false) #:nodoc: - method.to_s.ends_with? "?" + def respond_to_missing?(method, include_private = false) + (method.to_s.ends_with? "?") || super end end @@ -351,11 +326,11 @@ module Mime def ref; end - def respond_to_missing?(method, include_private = false) - method.to_s.ends_with? "?" - end - private + def respond_to_missing?(method, _) + method.to_s.ends_with? "?" + end + def method_missing(method, *args) false if method.to_s.ends_with? "?" end diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 01fe35f5c6..889f55a52a 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -50,7 +50,7 @@ module ActionDispatch def initialize(regexps, deep_regexps, blocks) @regexps = regexps @deep_regexps = deep_regexps.any? ? deep_regexps : nil - @blocks = blocks + @blocks = blocks end def call(original_params, parents = []) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 31ef0af791..7c585dbe68 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -12,8 +12,17 @@ module ActionDispatch } } + # Raised when raw data from the request cannot be parsed by the parser + # defined for request's content MIME type. + class ParseError < StandardError + def initialize + super($!.message) + end + end + included do class << self + # Returns the parameter parsers. attr_reader :parameter_parsers end @@ -21,7 +30,16 @@ module ActionDispatch end module ClassMethods - def parameter_parsers=(parsers) # :nodoc: + # Configure the parameter parser for a given MIME type. + # + # It accepts a hash where the key is the symbol of the MIME type + # and the value is a proc. + # + # original_parsers = ActionDispatch::Request.parameter_parsers + # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} } + # new_parsers = original_parsers.merge(xml: xml_parser) + # ActionDispatch::Request.parameter_parsers = new_parsers + def parameter_parsers=(parsers) @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } end end @@ -37,7 +55,7 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - params = set_custom_encoding(params) + params = set_binary_encoding(params) set_header("action_dispatch.request.parameters", params) params end @@ -65,19 +83,20 @@ module ActionDispatch private - def set_custom_encoding(params) + def set_binary_encoding(params) action = params[:action] - params.each do |k, v| - if v.is_a?(String) && v.encoding != encoding_template(action, k) - params[k] = v.force_encoding(encoding_template(action, k)) + if binary_params_for?(action) + ActionDispatch::Request::Utils.each_param_value(params) do |param| + param.force_encoding ::Encoding::ASCII_8BIT end end - params end - def encoding_template(action, param) - controller_class.encoding_for_param(action, param) + def binary_params_for?(action) + controller_class.binary_params_for?(action) + rescue NameError + false end def parse_formatted_parameters(parsers) @@ -87,11 +106,11 @@ module ActionDispatch begin strategy.call(raw_post) - rescue # JSON or Ruby code block errors + rescue # JSON or Ruby code block errors. my_logger = logger || ActiveSupport::Logger.new($stderr) my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" - raise ParamsParser::ParseError + raise ParseError end end @@ -100,4 +119,9 @@ module ActionDispatch end end end + + module ParamsParser + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + deprecate_constant "ParseError", "ActionDispatch::Http::Parameters::ParseError" + end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index e4ef9783f3..6d42404a98 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -69,7 +69,7 @@ module ActionDispatch PASS_NOT_FOUND = Class.new { # :nodoc: def self.action(_); self; end def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end - def self.encoding_for_param(action, param); ::Encoding::UTF_8; end + def self.binary_params_for?(action); false; end } def controller_class @@ -85,6 +85,9 @@ module ActionDispatch end end + # Returns true if the request has a header matching the given key parameter. + # + # request.key? :ip_spoofing_check # => true def key?(key) has_header? key end @@ -111,7 +114,7 @@ module ActionDispatch HTTP_METHOD_LOOKUP = {} - # Populate the HTTP method lookup cache + # Populate the HTTP method lookup cache. HTTP_METHODS.each { |method| HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym } @@ -162,12 +165,12 @@ module ActionDispatch def show_exceptions? # :nodoc: # We're treating `nil` as "unset", and we want the default setting to be - # `true`. This logic should be extracted to `env_config` and calculated + # `true`. This logic should be extracted to `env_config` and calculated # once. !(get_header("action_dispatch.show_exceptions".freeze) == false) end - # Returns a symbol form of the #request_method + # Returns a symbol form of the #request_method. def request_method_symbol HTTP_METHOD_LOOKUP[request_method] end @@ -179,7 +182,7 @@ module ActionDispatch @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD")) end - # Returns a symbol form of the #method + # Returns a symbol form of the #method. def method_symbol HTTP_METHOD_LOOKUP[method] end @@ -264,7 +267,7 @@ module ActionDispatch # (which sets the action_dispatch.request_id environment variable). # # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging. - # This relies on the rack variable set by the ActionDispatch::RequestId middleware. + # This relies on the Rack variable set by the ActionDispatch::RequestId middleware. def request_id get_header ACTION_DISPATCH_REQUEST_ID end @@ -336,7 +339,7 @@ module ActionDispatch Session::Options.set self, options end - # Override Rack's GET method to support indifferent access + # Override Rack's GET method to support indifferent access. def GET fetch_header("action_dispatch.request.query_parameters") do |k| rack_query_params = super || {} @@ -349,7 +352,7 @@ module ActionDispatch end alias :query_parameters :GET - # Override Rack's POST method to support indifferent access + # Override Rack's POST method to support indifferent access. def POST fetch_header("action_dispatch.request.request_parameters") do pr = parse_formatted_parameters(params_parsers) do |params| @@ -357,7 +360,7 @@ module ActionDispatch end self.request_parameters = Request::Utils.normalize_encode_params(pr) end - rescue ParamsParser::ParseError # one of the parse strategies blew up + rescue Http::Parameters::ParseError # one of the parse strategies blew up self.request_parameters = Request::Utils.normalize_encode_params(super || {}) raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index e8173e2a99..2ee52eeb48 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -39,7 +39,7 @@ module ActionDispatch # :nodoc: super(header) end - def []=(k,v) + def []=(k, v) if @response.sending? || @response.sent? raise ActionDispatch::IllegalStateError, "header already sent" end @@ -85,7 +85,7 @@ module ActionDispatch # :nodoc: cattr_accessor(:default_headers) include Rack::Response::Helpers - # Aliasing these off because AD::Http::Cache::Response defines them + # Aliasing these off because AD::Http::Cache::Response defines them. alias :_cache_control :cache_control alias :_cache_control= :cache_control= @@ -142,7 +142,7 @@ module ActionDispatch # :nodoc: private def each_chunk(&block) - @buf.each(&block) # extract into own method + @buf.each(&block) end end @@ -227,7 +227,9 @@ module ActionDispatch # :nodoc: return unless content_type new_header_info = parse_content_type(content_type.to_s) prev_header_info = parsed_content_type_header - set_content_type new_header_info.mime_type, new_header_info.charset || prev_header_info.charset || self.class.default_charset + charset = new_header_info.charset || prev_header_info.charset + charset ||= self.class.default_charset unless prev_header_info.mime_type + set_content_type new_header_info.mime_type, charset end # Sets the HTTP response's content MIME type. For example, in the controller @@ -249,7 +251,7 @@ module ActionDispatch # :nodoc: end end - # Sets the HTTP character set. In case of nil parameter + # Sets the HTTP character set. In case of +nil+ parameter # it sets the charset to utf-8. # # response.charset = 'utf-16' # => 'utf-16' @@ -408,7 +410,7 @@ module ActionDispatch # :nodoc: def parse_content_type(content_type) if content_type type, charset = content_type.split(/;\s*charset=/) - type = nil if type.empty? + type = nil if type && type.empty? ContentTypeHeader.new(type, charset) else NullContentTypeHeader @@ -423,7 +425,7 @@ module ActionDispatch # :nodoc: def set_content_type(content_type, charset) type = (content_type || "").dup - type << "; charset=#{charset}" if charset + type << "; charset=#{charset.to_s.downcase}" if charset set_header CONTENT_TYPE, type end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 9aa73c862b..225272d66e 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -24,23 +24,27 @@ module ActionDispatch attr_accessor :headers def initialize(hash) # :nodoc: - @tempfile = hash[:tempfile] + @tempfile = hash[:tempfile] raise(ArgumentError, ":tempfile is required") unless @tempfile - @original_filename = hash[:filename] - if @original_filename + if hash[:filename] + @original_filename = hash[:filename].dup + begin @original_filename.encode!(Encoding::UTF_8) rescue EncodingError @original_filename.force_encoding(Encoding::UTF_8) end + else + @original_filename = nil end + @content_type = hash[:type] @headers = hash[:head] end # Shortcut for +tempfile.read+. - def read(length=nil, buffer=nil) + def read(length = nil, buffer = nil) @tempfile.read(length, buffer) end @@ -50,7 +54,7 @@ module ActionDispatch end # Shortcut for +tempfile.close+. - def close(unlink_now=false) + def close(unlink_now = false) @tempfile.close(unlink_now) end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 06ffa983d1..b6be48a48b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -66,7 +66,7 @@ module ActionDispatch end def path_for(options) - path = options[:script_name].to_s.chomp("/".freeze) + path = options[:script_name].to_s.chomp("/".freeze) path << options[:path] if options.key?(:path) add_trailing_slash(path) if options[:trailing_slash] @@ -80,7 +80,7 @@ module ActionDispatch def add_params(path, params) params = { params: params } unless params.is_a?(Hash) - params.reject! { |_,v| v.to_param.nil? } + params.reject! { |_, v| v.to_param.nil? } query = params.to_query path << "?#{query}" unless query.empty? end @@ -101,10 +101,8 @@ module ActionDispatch end def add_trailing_slash(path) - # includes querysting if path.include?("?") path.sub!(/\?/, '/\&') - # does not have a .format elsif !path.include?(".") path.sub!(/[^\/]\z|\A\z/, '\&/') end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index a289c34e8b..326f4e52f9 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -1,10 +1,11 @@ require "action_controller/metal/exceptions" module ActionDispatch + # :stopdoc: module Journey # The Formatter class is used for formatting URLs. For example, parameters # passed to +url_for+ in Rails will eventually call Formatter#generate. - class Formatter # :nodoc: + class Formatter attr_reader :routes def initialize(routes) @@ -14,7 +15,7 @@ module ActionDispatch def generate(name, options, path_parameters, parameterize = nil) constraints = path_parameters.merge(options) - missing_keys = nil # need for variable scope + missing_keys = nil match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) @@ -35,7 +36,7 @@ module ActionDispatch route.parts.reverse_each do |key| break if defaults[key].nil? && parameterized_parts[key].present? - break if parameterized_parts[key].to_s != defaults[key].to_s + next if parameterized_parts[key].to_s != defaults[key].to_s break if required_parts.include?(key) parameterized_parts.delete(key) @@ -44,8 +45,12 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{Hash[constraints.sort_by { |k,v| k.to_s }].inspect}" - message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? + unmatched_keys = (missing_keys || []) & constraints.keys + missing_keys = (missing_keys || []) - unmatched_keys + + message = "No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}" + message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? + message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty? raise ActionController::UrlGenerationError, message end @@ -87,7 +92,11 @@ module ActionDispatch else routes = non_recursive(cache, options) - hash = routes.group_by { |_, r| r.score(options) } + supplied_keys = options.each_with_object({}) do |(k, v), h| + h[k.to_s] = true if v + end + + hash = routes.group_by { |_, r| r.score(supplied_keys) } hash.keys.sort.reverse_each do |score| break if score < 0 @@ -174,4 +183,5 @@ module ActionDispatch end end end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb index 9990c66627..0f8bed89bf 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb @@ -17,7 +17,7 @@ module ActionDispatch def transition_table dtrans = TransitionTable.new marked = {} - state_id = Hash.new { |h,k| h[k] = h.length } + state_id = Hash.new { |h, k| h[k] = h.length } start = firstpos(root) dstates = [start] diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb index d692f6415c..62f052ced6 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -18,14 +18,6 @@ module ActionDispatch @tt = transition_table end - def simulate(string) - ms = memos(string) { return } - MatchData.new(ms) - end - - alias :=~ :simulate - alias :match :simulate - def memos(string) input = StringScanner.new(string) state = [0] diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index 0be18dc26f..45aff287b1 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -12,7 +12,7 @@ module ActionDispatch @regexp_states = {} @string_states = {} @accepting = {} - @memos = Hash.new { |h,k| h[k] = [] } + @memos = Hash.new { |h, k| h[k] = [] } end def add_accepting(state) @@ -56,7 +56,7 @@ module ActionDispatch end def as_json(options = nil) - simple_regexp = Hash.new { |h,k| h[k] = {} } + simple_regexp = Hash.new { |h, k| h[k] = {} } @regexp_states.each do |from, hash| hash.each do |re, to| @@ -82,7 +82,7 @@ module ActionDispatch end def visualizer(paths, title = "FSM") - viz_dir = File.join File.dirname(__FILE__), "..", "visualizer" + viz_dir = File.join __dir__, "..", "visualizer" fsm_js = File.read File.join(viz_dir, "fsm.js") fsm_css = File.read File.join(viz_dir, "fsm.css") erb = File.read File.join(viz_dir, "index.html.erb") @@ -109,7 +109,6 @@ module ActionDispatch svg = to_svg javascripts = [states, fsm_js] - # Annoying hack warnings fun_routes = fun_routes stylesheets = stylesheets svg = svg diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb index 19e5752ae5..532f765094 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/builder.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb @@ -36,7 +36,7 @@ module ActionDispatch def visit_OR(node) from = @i += 1 children = node.children.map { |c| visit(c) } - to = @i += 1 + to = @i += 1 children.each do |child| @tt[from, child.first] = nil diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb index 4737adc724..543a670da0 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -10,7 +10,7 @@ module ActionDispatch attr_reader :memos def initialize - @table = Hash.new { |h,f| h[f] = {} } + @table = Hash.new { |h, f| h[f] = {} } @memos = {} @accepting = nil @inverted = nil diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index 01ff2109cb..e002755bcf 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,196 +1,199 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.11 -# from Racc grammer file "". +# This file is automatically generated by Racc 1.4.14 +# from Racc grammar file "". # -require "racc/parser.rb" +require 'racc/parser.rb' + +# :stopdoc: require "action_dispatch/journey/parser_extras" module ActionDispatch module Journey class Parser < Racc::Parser - ##### State transition tables begin ### - - racc_action_table = [ - 13, 15, 14, 7, 21, 16, 8, 19, 13, 15, - 14, 7, 17, 16, 8, 13, 15, 14, 7, 24, - 16, 8, 13, 15, 14, 7, 19, 16, 8 ] - - racc_action_check = [ - 2, 2, 2, 2, 17, 2, 2, 2, 0, 0, - 0, 0, 1, 0, 0, 19, 19, 19, 19, 20, - 19, 19, 7, 7, 7, 7, 22, 7, 7 ] - - racc_action_pointer = [ - 6, 12, -2, nil, nil, nil, nil, 20, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 4, nil, 13, - 13, nil, 17, nil, nil ] - - racc_action_default = [ - -19, -19, -2, -3, -4, -5, -6, -19, -10, -11, - -12, -13, -14, -15, -16, -17, -18, -19, -1, -19, - -19, 25, -8, -9, -7 ] - - racc_goto_table = [ - 1, 22, 18, 23, nil, nil, nil, 20 ] - - racc_goto_check = [ - 1, 2, 1, 3, nil, nil, nil, 1 ] - - racc_goto_pointer = [ - nil, 0, -18, -16, nil, nil, nil, nil, nil, nil, - nil ] - - racc_goto_default = [ - nil, nil, 2, 3, 4, 5, 6, 9, 10, 11, - 12 ] - - racc_reduce_table = [ - 0, 0, :racc_error, - 2, 11, :_reduce_1, - 1, 11, :_reduce_2, - 1, 11, :_reduce_none, - 1, 12, :_reduce_none, - 1, 12, :_reduce_none, - 1, 12, :_reduce_none, - 3, 15, :_reduce_7, - 3, 13, :_reduce_8, - 3, 13, :_reduce_9, - 1, 16, :_reduce_10, - 1, 14, :_reduce_none, - 1, 14, :_reduce_none, - 1, 14, :_reduce_none, - 1, 14, :_reduce_none, - 1, 19, :_reduce_15, - 1, 17, :_reduce_16, - 1, 18, :_reduce_17, - 1, 20, :_reduce_18 ] - - racc_reduce_n = 19 - - racc_shift_n = 25 - - racc_token_table = { - false => 0, - :error => 1, - :SLASH => 2, - :LITERAL => 3, - :SYMBOL => 4, - :LPAREN => 5, - :RPAREN => 6, - :DOT => 7, - :STAR => 8, - :OR => 9 } - - racc_nt_base = 10 - - racc_use_result_var = false - - Racc_arg = [ - racc_action_table, - racc_action_check, - racc_action_default, - racc_action_pointer, - racc_goto_table, - racc_goto_check, - racc_goto_default, - racc_goto_pointer, - racc_nt_base, - racc_reduce_table, - racc_token_table, - racc_shift_n, - racc_reduce_n, - racc_use_result_var ] - - Racc_token_to_s_table = [ - "$end", - "error", - "SLASH", - "LITERAL", - "SYMBOL", - "LPAREN", - "RPAREN", - "DOT", - "STAR", - "OR", - "$start", - "expressions", - "expression", - "or", - "terminal", - "group", - "star", - "symbol", - "literal", - "slash", - "dot" ] - - Racc_debug_parser = false - - ##### State transition tables end ##### - - # reduce 0 omitted - - def _reduce_1(val, _values) - Cat.new(val.first, val.last) - end - - def _reduce_2(val, _values) - val.first - end - - # reduce 3 omitted - - # reduce 4 omitted - - # reduce 5 omitted - - # reduce 6 omitted - - def _reduce_7(val, _values) - Group.new(val[1]) - end - - def _reduce_8(val, _values) - Or.new([val.first, val.last]) - end - - def _reduce_9(val, _values) - Or.new([val.first, val.last]) - end - - def _reduce_10(val, _values) - Star.new(Symbol.new(val.last)) - end - - # reduce 11 omitted - - # reduce 12 omitted - - # reduce 13 omitted - - # reduce 14 omitted - - def _reduce_15(val, _values) - Slash.new("/") - end - - def _reduce_16(val, _values) - Symbol.new(val.first) - end - - def _reduce_17(val, _values) - Literal.new(val.first) - end - - def _reduce_18(val, _values) - Dot.new(val.first) - end - - def _reduce_none(val, _values) - val[0] - end +##### State transition tables begin ### + +racc_action_table = [ + 13, 15, 14, 7, 19, 16, 8, 19, 13, 15, + 14, 7, 17, 16, 8, 13, 15, 14, 7, 21, + 16, 8, 13, 15, 14, 7, 24, 16, 8 ] + +racc_action_check = [ + 2, 2, 2, 2, 22, 2, 2, 2, 19, 19, + 19, 19, 1, 19, 19, 7, 7, 7, 7, 17, + 7, 7, 0, 0, 0, 0, 20, 0, 0 ] + +racc_action_pointer = [ + 20, 12, -2, nil, nil, nil, nil, 13, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 19, nil, 6, + 20, nil, -5, nil, nil ] + +racc_action_default = [ + -19, -19, -2, -3, -4, -5, -6, -19, -10, -11, + -12, -13, -14, -15, -16, -17, -18, -19, -1, -19, + -19, 25, -8, -9, -7 ] + +racc_goto_table = [ + 1, 22, 18, 23, nil, nil, nil, 20 ] + +racc_goto_check = [ + 1, 2, 1, 3, nil, nil, nil, 1 ] + +racc_goto_pointer = [ + nil, 0, -18, -16, nil, nil, nil, nil, nil, nil, + nil ] + +racc_goto_default = [ + nil, nil, 2, 3, 4, 5, 6, 9, 10, 11, + 12 ] + +racc_reduce_table = [ + 0, 0, :racc_error, + 2, 11, :_reduce_1, + 1, 11, :_reduce_2, + 1, 11, :_reduce_none, + 1, 12, :_reduce_none, + 1, 12, :_reduce_none, + 1, 12, :_reduce_none, + 3, 15, :_reduce_7, + 3, 13, :_reduce_8, + 3, 13, :_reduce_9, + 1, 16, :_reduce_10, + 1, 14, :_reduce_none, + 1, 14, :_reduce_none, + 1, 14, :_reduce_none, + 1, 14, :_reduce_none, + 1, 19, :_reduce_15, + 1, 17, :_reduce_16, + 1, 18, :_reduce_17, + 1, 20, :_reduce_18 ] + +racc_reduce_n = 19 + +racc_shift_n = 25 + +racc_token_table = { + false => 0, + :error => 1, + :SLASH => 2, + :LITERAL => 3, + :SYMBOL => 4, + :LPAREN => 5, + :RPAREN => 6, + :DOT => 7, + :STAR => 8, + :OR => 9 } + +racc_nt_base = 10 + +racc_use_result_var = false + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ + "$end", + "error", + "SLASH", + "LITERAL", + "SYMBOL", + "LPAREN", + "RPAREN", + "DOT", + "STAR", + "OR", + "$start", + "expressions", + "expression", + "or", + "terminal", + "group", + "star", + "symbol", + "literal", + "slash", + "dot" ] + +Racc_debug_parser = false + +##### State transition tables end ##### + +# reduce 0 omitted + +def _reduce_1(val, _values) + Cat.new(val.first, val.last) +end + +def _reduce_2(val, _values) + val.first +end + +# reduce 3 omitted + +# reduce 4 omitted + +# reduce 5 omitted + +# reduce 6 omitted + +def _reduce_7(val, _values) + Group.new(val[1]) +end + +def _reduce_8(val, _values) + Or.new([val.first, val.last]) +end + +def _reduce_9(val, _values) + Or.new([val.first, val.last]) +end + +def _reduce_10(val, _values) + Star.new(Symbol.new(val.last)) +end + +# reduce 11 omitted + +# reduce 12 omitted + +# reduce 13 omitted + +# reduce 14 omitted + +def _reduce_15(val, _values) + Slash.new(val.first) +end + +def _reduce_16(val, _values) + Symbol.new(val.first) +end + +def _reduce_17(val, _values) + Literal.new(val.first) +end + +def _reduce_18(val, _values) + Dot.new(val.first) +end + +def _reduce_none(val, _values) + val[0] +end + end # class Parser - end # module Journey -end # module ActionDispatch + end # module Journey + end # module ActionDispatch diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index d3f7c4d765..f9b1a7a958 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -30,7 +30,7 @@ rule | dot ; slash - : SLASH { Slash.new('/') } + : SLASH { Slash.new(val.first) } ; symbol : SYMBOL { Symbol.new(val.first) } @@ -45,5 +45,6 @@ rule end ---- header +# :stopdoc: -require 'action_dispatch/journey/parser_extras' +require "action_dispatch/journey/parser_extras" diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb index ec26e634e8..4c7e82d93c 100644 --- a/actionpack/lib/action_dispatch/journey/parser_extras.rb +++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb @@ -2,8 +2,9 @@ require "action_dispatch/journey/scanner" require "action_dispatch/journey/nodes/node" module ActionDispatch - module Journey # :nodoc: - class Parser < Racc::Parser # :nodoc: + # :stopdoc: + module Journey + class Parser < Racc::Parser include Journey::Nodes def self.parse(string) @@ -24,4 +25,5 @@ module ActionDispatch end end end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 0902b9233e..cf0108ec32 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -31,6 +31,13 @@ module ActionDispatch Visitors::FormatBuilder.new.accept(spec) end + def eager_load! + required_names + offsets + to_regexp + nil + end + def ast @spec.find_all(&:symbol?).each do |node| re = @requirements[node.to_sym] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index a9713ff292..0acbac1d9d 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -1,6 +1,7 @@ module ActionDispatch - module Journey # :nodoc: - class Route # :nodoc: + # :stopdoc: + module Journey + class Route attr_reader :app, :path, :defaults, :name, :precedence attr_reader :constraints, :internal @@ -9,11 +10,11 @@ module ActionDispatch module VerbMatchers VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK } VERBS.each do |v| - class_eval <<-eoc - class #{v} - def self.verb; name.split("::").last; end - def self.call(req); req.#{v.downcase}?; end - end + class_eval <<-eoc, __FILE__, __LINE__ + 1 + class #{v} + def self.verb; name.split("::").last; end + def self.call(req); req.#{v.downcase}?; end + end eoc end @@ -72,6 +73,14 @@ module ActionDispatch @internal = internal end + def eager_load! + path.eager_load! + ast + parts + required_defaults + nil + end + def ast @decorated_ast ||= begin decorated_ast = path.ast @@ -80,9 +89,16 @@ module ActionDispatch end end - def requirements # :nodoc: - # needed for rails `rails routes` - @defaults.merge(path.requirements).delete_if { |_,v| + # Needed for `rails routes`. Picks up succinctly defined requirements + # for a route, for example route + # + # get 'photo/:id', :controller => 'photos', :action => 'show', + # :id => /[A-Z]\d{5}/ + # + # will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/} + # as requirements. + def requirements + @defaults.merge(path.requirements).delete_if { |_, v| /.+?/ == v } end @@ -95,13 +111,18 @@ module ActionDispatch required_parts + required_defaults.keys end - def score(constraints) + def score(supplied_keys) required_keys = path.required_names - supplied_keys = constraints.map { |k,v| v && k.to_s }.compact - return -1 unless (required_keys - supplied_keys).empty? + required_keys.each do |k| + return -1 unless supplied_keys.include?(k) + end + + score = 0 + path.names.each do |k| + score += 1 if supplied_keys.include?(k) + end - score = (supplied_keys & path.names).length score + (required_defaults.length * 2) end @@ -123,7 +144,7 @@ module ActionDispatch end def required_defaults - @required_defaults ||= @defaults.dup.delete_if do |k,_| + @required_defaults ||= @defaults.dup.delete_if do |k, _| parts.include?(k) || !required_default?(k) end end @@ -176,4 +197,5 @@ module ActionDispatch end end end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index d0ef549335..d55e1399e4 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -22,6 +22,13 @@ module ActionDispatch @routes = routes end + def eager_load! + # Eagerly trigger the simulator's initialization so + # it doesn't happen during a request cycle. + simulator + nil + end + def serve(req) find_routes(req).each do |match, parameters, route| set_params = req.path_parameters @@ -109,9 +116,9 @@ module ActionDispatch routes.sort_by!(&:precedence) routes.map! { |r| - match_data = r.path.match(req.path_info) + match_data = r.path.match(req.path_info) path_parameters = r.defaults.dup - match_data.names.zip(match_data.captures) { |name,val| + match_data.names.zip(match_data.captures) { |name, val| path_parameters[name.to_sym] = Utils.unescape_uri(val) if val } [match_data, path_parameters, r] diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index ce5d350763..6d400f3364 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -5,7 +5,7 @@ module ActionDispatch # Normalizes URI path. # # Strips off trailing slash and ensures there is a leading slash. - # Also converts downcase url encoded string to uppercase. + # Also converts downcase URL encoded string to uppercase. # # normalize_path("/foo") # => "/foo" # normalize_path("/foo/") # => "/foo" @@ -13,11 +13,13 @@ module ActionDispatch # normalize_path("") # => "/" # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) + encoding = path.encoding path = "/#{path}" path.squeeze!("/".freeze) path.sub!(%r{/+\Z}, "".freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = "/" if path == "".freeze + path.force_encoding(encoding) path end @@ -58,7 +60,7 @@ module ActionDispatch uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding) end - protected + private def escape(component, pattern) component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII) end @@ -84,6 +86,10 @@ module ActionDispatch ENCODER.escape_fragment(fragment.to_s) end + # Replaces any escaped sequences with their unescaped representations. + # + # uri = "/topics?title=Ruby%20on%20Rails" + # unescape_uri(uri) #=> "/topics?title=Ruby on Rails" def self.unescape_uri(uri) ENCODER.unescape_uri(uri) end diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb index 4b8c8ab063..7dbb39b26d 100644 --- a/actionpack/lib/action_dispatch/journey/scanner.rb +++ b/actionpack/lib/action_dispatch/journey/scanner.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "strscan" module ActionDispatch @@ -35,22 +36,23 @@ module ActionDispatch def scan case # / - when text = @ss.scan(/\//) - [:SLASH, text] + when @ss.skip(/\//) + [:SLASH, "/"] + when @ss.skip(/\(/) + [:LPAREN, "("] + when @ss.skip(/\)/) + [:RPAREN, ")"] + when @ss.skip(/\|/) + [:OR, "|"] + when @ss.skip(/\./) + [:DOT, "."] + when text = @ss.scan(/:\w+/) + [:SYMBOL, text] when text = @ss.scan(/\*\w+/) [:STAR, text] - when text = @ss.scan(/(?<!\\)\(/) - [:LPAREN, text] - when text = @ss.scan(/(?<!\\)\)/) - [:RPAREN, text] - when text = @ss.scan(/\|/) - [:OR, text] - when text = @ss.scan(/\./) - [:DOT, text] - when text = @ss.scan(/(?<!\\):\w+/) - [:SYMBOL, text] - when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/) - [:LITERAL, text.tr('\\', "")] + when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/) + text.tr! "\\", "" + [:LITERAL, text] # any char when text = @ss.scan(/./) [:LITERAL, text] diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 452dc84cc5..335797f4b9 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,10 +1,11 @@ module ActionDispatch - module Journey # :nodoc: + # :stopdoc: + module Journey class Format ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } - class Parameter < Struct.new(:name, :escaper) + Parameter = Struct.new(:name, :escaper) do def escape(value); escaper.call value; end end @@ -21,7 +22,7 @@ module ActionDispatch @children = [] @parameters = [] - parts.each_with_index do |object,i| + parts.each_with_index do |object, i| case object when Journey::Format @children << i @@ -153,7 +154,7 @@ module ActionDispatch end end - # Loop through the requirements AST + # Loop through the requirements AST. class Each < FunctionalVisitor # :nodoc: def visit(node, block) block.call(node) @@ -261,4 +262,5 @@ module ActionDispatch end end end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index fef246532b..ff129cf96a 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,4 +1,3 @@ - module ActionDispatch # Provides callbacks to be executed before and after dispatching the request. class Callbacks @@ -7,17 +6,6 @@ module ActionDispatch define_callbacks :call class << self - def to_prepare(*args, &block) - ActiveSupport::Reloader.to_prepare(*args, &block) - end - - def to_cleanup(*args, &block) - ActiveSupport::Reloader.to_complete(*args, &block) - end - - deprecate to_prepare: "use ActiveSupport::Reloader.to_prepare instead", - to_cleanup: "use ActiveSupport::Reloader.to_complete instead" - def before(*args, &block) set_callback(:call, :before, *args, &block) end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 6f4fab396a..c0dda1bba5 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -43,6 +43,10 @@ module ActionDispatch get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end + def authenticated_encrypted_cookie_salt + get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT + end + def secret_token get_header Cookies::SECRET_TOKEN end @@ -149,6 +153,7 @@ module ActionDispatch SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze @@ -160,7 +165,7 @@ module ActionDispatch # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError - # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed + # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed. module ChainedCookieJars # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: # @@ -179,7 +184,7 @@ module ActionDispatch # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed - # cookie was tampered with by the user (or a 3rd party), nil will be returned. + # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. @@ -202,11 +207,14 @@ module ActionDispatch end # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. - # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. + # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # + # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ + # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. + # # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: @@ -219,6 +227,8 @@ module ActionDispatch @encrypted ||= if upgrade_legacy_signed_cookies? UpgradeLegacyEncryptedCookieJar.new(self) + elsif upgrade_legacy_hmac_aes_cbc_cookies? + UpgradeLegacyHmacAesCbcCookieJar.new(self) else EncryptedCookieJar.new(self) end @@ -240,6 +250,13 @@ module ActionDispatch def upgrade_legacy_signed_cookies? request.secret_token.present? && request.secret_key_base.present? end + + def upgrade_legacy_hmac_aes_cbc_cookies? + request.secret_key_base.present? && + request.authenticated_encrypted_cookie_salt.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -332,29 +349,29 @@ module ActionDispatch def update_cookies_from_jar request_jar = @request.cookie_jar.instance_variable_get(:@cookies) - set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) } + set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) } @cookies.update set_cookies if set_cookies end def to_header - @cookies.map { |k,v| "#{escape(k)}=#{escape(v)}" }.join "; " + @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; " end def handle_options(options) #:nodoc: options[:path] ||= "/" if options[:domain] == :all || options[:domain] == "all" - # if there is a provided tld length then we use it otherwise default domain regexp + # If there is a provided tld length then we use it otherwise default domain regexp. domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP - # if host is not ip and matches domain regexp + # If host is not ip and matches domain regexp. # (ip confirms to domain regexp so we explicitly check for ip) options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp) ".#{$&}" end elsif options[:domain].is_a? Array - # if host matches one of the supplied domains without a dot in front of it + # If host matches one of the supplied domains without a dot in front of it. options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") } end end @@ -404,7 +421,7 @@ module ActionDispatch @delete_cookies[name.to_s] == options end - # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie + # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie. def clear(options = {}) @cookies.each_key { |k| delete(k, options) } end @@ -572,13 +589,15 @@ module ActionDispatch super if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " \ "Read the upgrade documentation to learn more about this new config option." end - secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] - sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + cipher = "aes-256-gcm" + key_len = ActiveSupport::MessageEncryptor.key_len(cipher) + secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len] + + @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end private @@ -603,6 +622,32 @@ module ActionDispatch include VerifyAndUpgradeLegacySignedMessage end + # UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore + # to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM + class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar + def initialize(parent_jar) + super + + secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") + + @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + end + + def decrypt_and_verify_legacy_encrypted_message(name, signed_message) + deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value| + self[name] = { value: value } + end + rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage + nil + end + + private + def parse(name, signed_message) + super || decrypt_and_verify_legacy_encrypted_message(name, signed_message) + end + end + def initialize(app) @app = app end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index ee644f41c8..336a775880 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -10,7 +10,7 @@ module ActionDispatch # This middleware is responsible for logging exceptions and # showing a debugging page in case the request is local. class DebugExceptions - RESCUES_TEMPLATE_PATH = File.expand_path("../templates", __FILE__) + RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__) class DebugView < ActionView::Base def debug_params(params) @@ -38,7 +38,9 @@ module ActionDispatch end def render(*) - if logger = ActionView::Base.logger + logger = ActionView::Base.logger + + if logger && logger.respond_to?(:silence) logger.silence { super } else super @@ -173,7 +175,11 @@ module ActionDispatch end def log_array(logger, array) - array.map { |line| logger.fatal line } + if logger.formatter && logger.formatter.respond_to?(:tags_text) + logger.fatal array.join("\n#{logger.formatter.tags_text}") + else + logger.fatal array.join("\n") + end end def logger(request) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 9b44c4483e..397f0a8b92 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -6,19 +6,19 @@ module ActionDispatch cattr_accessor :rescue_responses @@rescue_responses = Hash.new(:internal_server_error) @@rescue_responses.merge!( - "ActionController::RoutingError" => :not_found, - "AbstractController::ActionNotFound" => :not_found, - "ActionController::MethodNotAllowed" => :method_not_allowed, - "ActionController::UnknownHttpMethod" => :method_not_allowed, - "ActionController::NotImplemented" => :not_implemented, - "ActionController::UnknownFormat" => :not_acceptable, - "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, - "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, - "ActionDispatch::ParamsParser::ParseError" => :bad_request, - "ActionController::BadRequest" => :bad_request, - "ActionController::ParameterMissing" => :bad_request, - "Rack::QueryParser::ParameterTypeError" => :bad_request, - "Rack::QueryParser::InvalidParameterError" => :bad_request + "ActionController::RoutingError" => :not_found, + "AbstractController::ActionNotFound" => :not_found, + "ActionController::MethodNotAllowed" => :method_not_allowed, + "ActionController::UnknownHttpMethod" => :method_not_allowed, + "ActionController::NotImplemented" => :not_implemented, + "ActionController::UnknownFormat" => :not_acceptable, + "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, + "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, + "ActionDispatch::Http::Parameters::ParseError" => :bad_request, + "ActionController::BadRequest" => :bad_request, + "ActionController::ParameterMissing" => :bad_request, + "Rack::QueryParser::ParameterTypeError" => :bad_request, + "Rack::QueryParser::InvalidParameterError" => :bad_request ) cattr_accessor :rescue_templates @@ -127,7 +127,7 @@ module ActionDispatch File.open(full_path, "r") do |file| start = [line - 3, 0].max lines = file.each_line.drop(start).take(6) - Hash[*(start+1..(lines.count+start)).zip(lines).flatten] + Hash[*(start + 1..(lines.count + start)).zip(lines).flatten] end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 6900934712..6b29ce63ba 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -65,7 +65,7 @@ module ActionDispatch self.flash = flash_hash.dup end - if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded? session.key?("flash") && session["flash"].nil? session.delete("flash") end @@ -129,7 +129,7 @@ module ActionDispatch end # Builds a hash containing the flashes to keep for the next request. - # If there are none to keep, returns nil. + # If there are none to keep, returns +nil+. def to_session_value #:nodoc: flashes_to_keep = @flashes.except(*@discard) return nil if flashes_to_keep.empty? @@ -281,7 +281,8 @@ module ActionDispatch @now end - def stringify_array(array) + private + def stringify_array(array) # :doc: array.map do |item| item.kind_of?(Symbol) ? item.to_s : item end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb deleted file mode 100644 index 5f96b80e87..0000000000 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ /dev/null @@ -1,45 +0,0 @@ -require "action_dispatch/http/request" - -module ActionDispatch - # ActionDispatch::ParamsParser works for all the requests having any Content-Length - # (like POST). It takes raw data from the request and puts it through the parser - # that is picked based on Content-Type header. - # - # In case of any error while parsing data ParamsParser::ParseError is raised. - class ParamsParser - # Raised when raw data from the request cannot be parsed by the parser - # defined for request's content mime type. - class ParseError < StandardError - def initialize(message = nil, original_exception = nil) - if message - ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \ - "#{self.class} will automatically capture the message " \ - "of the original exception.", caller) - end - - if original_exception - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - - super($!.message) - end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end - end - - # Create a new +ParamsParser+ middleware instance. - # - # The +parsers+ argument can take Hash of parsers where key is identifying - # content mime type, and value is a lambda that is going to process data. - def self.new(app, parsers = {}) - ActiveSupport::Deprecation.warn("ActionDispatch::ParamsParser is deprecated and will be removed in Rails 5.1. Configure the parameter parsing in ActionDispatch::Request.parameter_parsers.") - parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } - ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) - app - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 90c64037aa..6d64b1424b 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,54 +1,10 @@ module ActionDispatch - # ActionDispatch::Reloader provides prepare and cleanup callbacks, - # intended to assist with code reloading during development. - # - # Prepare callbacks are run before each request, and cleanup callbacks - # after each request. In this respect they are analogs of ActionDispatch::Callback's - # before and after callbacks. However, cleanup callbacks are not called until the - # request is fully complete -- that is, after #close has been called on - # the response body. This is important for streaming responses such as the - # following: - # - # self.response_body = -> (response, output) do - # # code here which refers to application models - # end - # - # Cleanup callbacks will not be called until after the response_body lambda - # is evaluated, ensuring that it can refer to application models and other - # classes before they are unloaded. + # ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader + # callbacks, intended to assist with code reloading during development. # # By default, ActionDispatch::Reloader is included in the middleware stack # only in the development environment; specifically, when +config.cache_classes+ - # is false. Callbacks may be registered even when it is not included in the - # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt> - # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually. - # + # is false. class Reloader < Executor - def self.to_prepare(*args, &block) - ActiveSupport::Reloader.to_prepare(*args, &block) - end - - def self.to_cleanup(*args, &block) - ActiveSupport::Reloader.to_complete(*args, &block) - end - - def self.prepare! - default_reloader.prepare! - end - - def self.cleanup! - default_reloader.reload! - end - - class << self - attr_accessor :default_reloader # :nodoc: - - deprecate to_prepare: "use ActiveSupport::Reloader.to_prepare instead", - to_cleanup: "use ActiveSupport::Reloader.to_complete instead", - prepare!: "use Rails.application.reloader.prepare! instead", - cleanup!: "use Rails.application.reloader.reload! instead of cleanup + prepare" - end - - self.default_reloader = ActiveSupport::Reloader end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 523eeb5b05..53d5a4918c 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -131,8 +131,8 @@ module ActionDispatch should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user - raise IpSpoofAttackError, "IP spoofing attack?! " + - "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + raise IpSpoofAttackError, "IP spoofing attack?! " \ + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end @@ -153,17 +153,17 @@ module ActionDispatch @ip ||= calculate_ip end - protected + private - def ips_from(header) + def ips_from(header) # :doc: return [] unless header - # Split the comma-separated list into an array of strings + # Split the comma-separated list into an array of strings. ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin - # Only return IPs that are valid according to the IPAddr#new method + # Only return IPs that are valid according to the IPAddr#new method. range = IPAddr.new(ip).to_range - # we want to make sure nobody is sneaking a netmask in + # We want to make sure nobody is sneaking a netmask in. range.begin == range.end rescue ArgumentError nil @@ -171,7 +171,7 @@ module ActionDispatch end end - def filter_proxies(ips) + def filter_proxies(ips) # :doc: ips.reject do |ip| @proxies.any? { |proxy| proxy === ip } end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index bd4c781267..1925ffd9dd 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -2,8 +2,9 @@ require "securerandom" require "active_support/core_ext/string/access" module ActionDispatch - # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through - # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header. + # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible + # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends + # the same id to the client via the X-Request-Id header. # # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the @@ -12,7 +13,7 @@ module ActionDispatch # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files # from multiple pieces of the stack. class RequestId - X_REQUEST_ID = "X-Request-Id".freeze # :nodoc: + X_REQUEST_ID = "X-Request-Id".freeze #:nodoc: def initialize(app) @app = app diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 60920ea6c8..21ccf5a097 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -7,22 +7,12 @@ require "action_dispatch/request/session" module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - def initialize(const_error = nil) - if const_error - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - - super("Session contains objects whose class definition isn't available.\n" + - "Remember to require the classes for all objects kept in the session.\n" + + def initialize + super("Session contains objects whose class definition isn't available.\n" \ + "Remember to require the classes for all objects kept in the session.\n" \ "(Original exception: #{$!.message} [#{$!.class}])\n") set_backtrace $!.backtrace end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end module Compatibility @@ -37,17 +27,16 @@ module ActionDispatch sid end - protected + private - def initialize_sid + def initialize_sid # :doc: @default_options.delete(:sidbits) @default_options.delete(:secure_random) end - private - def make_request(env) - ActionDispatch::Request.new env - end + def make_request(env) + ActionDispatch::Request.new env + end end module StaleSessionCheck @@ -64,7 +53,7 @@ module ActionDispatch rescue ArgumentError => argument_error if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} begin - # Note that the regexp does not allow $1 to end with a ':' + # Note that the regexp does not allow $1 to end with a ':'. $1.constantize rescue LoadError, NameError raise ActionDispatch::Session::SessionRestoreError diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 8409109ede..57d325a9d8 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -63,7 +63,7 @@ module ActionDispatch # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and # <tt>:httponly</tt>. class CookieStore < AbstractStore - def initialize(app, options={}) + def initialize(app, options = {}) super(app, options.merge!(cookie_only: true)) end @@ -102,7 +102,7 @@ module ActionDispatch end end - def persistent_session_id!(data, sid=nil) + def persistent_session_id!(data, sid = nil) data ||= {} data["session_id"] ||= sid || generate_sid data diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 90f26a1c33..5a99714ec2 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -8,7 +8,7 @@ module ActionDispatch # The exceptions app should be passed as parameter on initialization # of ShowExceptions. Every time there is an exception, ShowExceptions will # store the exception in env["action_dispatch.exception"], rewrite the - # PATH_INFO to the exception status code and call the rack app. + # PATH_INFO to the exception status code and call the Rack app. # # If the application returns a "X-Cascade" pass response, this middleware # will send an empty response as result with the correct status code. diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 992daab3aa..557721c301 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -23,7 +23,7 @@ module ActionDispatch # `180.days` (recommended). # * `subdomains`: Set to `true` to tell the browser to apply these settings # to all subdomains. This protects your cookies from interception by a - # vulnerable site on a subdomain. Defaults to `false`. + # vulnerable site on a subdomain. Defaults to `true`. # * `preload`: Advertise that this site may be included in browsers' # preloaded HSTS lists. HSTS protects your site on every visit *except the # first visit* since it hasn't seen your HSTS header yet. To close this @@ -45,35 +45,17 @@ module ActionDispatch HSTS_EXPIRES_IN = 15552000 def self.default_hsts_options - { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } + { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false } end - def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true) @app = app - if options[:host] || options[:port] - ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc - The `:host` and `:port` options are moving within `:redirect`: - `config.ssl_options = { redirect: { host: …, port: … } }`. - end_warning - @redirect = options.slice(:host, :port) - else - @redirect = redirect - end + @redirect = redirect @exclude = @redirect && @redirect[:exclude] || proc { !@redirect } @secure_cookies = secure_cookies - if hsts != true && hsts != false && hsts[:subdomains].nil? - hsts[:subdomains] = false - - ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc - In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if - unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out - of this behavior. - end_warning - end - @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 466eb8b3f1..6949b31e75 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -88,7 +88,6 @@ module ActionDispatch end def delete(target) - target = get_class target middlewares.delete_if { |m| m.klass == target } end @@ -103,31 +102,13 @@ module ActionDispatch private def assert_index(index, where) - index = get_class index i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index } raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end - def get_class(klass) - if klass.is_a?(String) || klass.is_a?(Symbol) - classcache = ActiveSupport::Dependencies::Reference - converted_klass = classcache[klass.to_s] - ActiveSupport::Deprecation.warn <<-eowarn -Passing strings or symbols to the middleware builder is deprecated, please change -them to actual class references. For example: - - "#{klass}" => #{converted_klass} - - eowarn - converted_klass - else - klass - end - end - def build_middleware(klass, args, block) - Middleware.new(get_class(klass), args, block) + Middleware.new(klass, args, block) end end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index fbf2a5fd0b..5d10129d21 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -33,7 +33,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.force_encoding("UTF-8".freeze)) + path = File.join(@root, p.force_encoding(Encoding::UTF_8)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -106,14 +106,7 @@ module ActionDispatch # produce a directory traversal using this middleware. Only 'GET' and 'HEAD' # requests will result in a file being returned. class Static - def initialize(app, path, deprecated_cache_control = :not_set, index: "index", headers: {}) - if deprecated_cache_control != :not_set - ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \ - "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \ - " and will be removed in Rails 5.1.") - headers["Cache-Control".freeze] = deprecated_cache_control - end - + def initialize(app, path, index: "index", headers: {}) @app = app @file_handler = FileHandler.new(path, index: index, headers: headers) end diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 429ea7057c..2d21ae63f5 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -60,7 +60,7 @@ <%= link_to "Path", "#", 'data-route-helper' => '_path', title: "Returns a relative path (without the http or domain)" %> / <%= link_to "Url", "#", 'data-route-helper' => '_url', - title: "Returns an absolute url (with the http and domain)" %> + title: "Returns an absolute URL (with the http and domain)" %> </th> <th><%# HTTP Verb %> </th> @@ -93,7 +93,7 @@ } } - // get JSON from url and invoke callback with result + // get JSON from URL and invoke callback with result function getJSON(url, success) { var xhr = new XMLHttpRequest(); xhr.open('GET', url); diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 48cc91bbfa..7662e164b8 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = "signed cookie" config.action_dispatch.encrypted_cookie_salt = "encrypted cookie" config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" + config.action_dispatch.use_authenticated_cookie_encryption = false config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { @@ -36,11 +37,11 @@ module ActionDispatch ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) + config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption + config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie - ActionDispatch::Reloader.default_reloader = app.reloader - ActionDispatch.test_app = app end end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index b883ca0f61..3547a8604f 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -7,10 +7,10 @@ module ActionDispatch ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc: - # Singleton object used to determine if an optional param wasn't specified + # Singleton object used to determine if an optional param wasn't specified. Unspecified = Object.new - # Creates a session hash, merging the properties of the previous session if any + # Creates a session hash, merging the properties of the previous session if any. def self.create(store, req, default_options) session_was = find req session = Request::Session.new(store, req) @@ -53,7 +53,7 @@ module ActionDispatch } end - def []=(k,v); @delegate[k] = v; end + def []=(k, v); @delegate[k] = v; end def to_hash; @delegate.dup; end def values_at(*args); @delegate.values_at(*args); end end @@ -63,7 +63,7 @@ module ActionDispatch @req = req @delegate = {} @loaded = false - @exists = nil # we haven't checked yet + @exists = nil # We haven't checked yet. end def id @@ -79,13 +79,13 @@ module ActionDispatch options = self.options || {} @by.send(:delete_session, @req, options.id(@req), options) - # Load the new sid to be written with the response + # Load the new sid to be written with the response. @loaded = false load_for_write! end # Returns value of the key stored in the session or - # nil if the given key is not found in the session. + # +nil+ if the given key is not found in the session. def [](key) load_for_read! @delegate[key.to_s] @@ -101,11 +101,13 @@ module ActionDispatch # Returns keys of the session as Array. def keys + load_for_read! @delegate.keys end # Returns values of the session as Array. def values + load_for_read! @delegate.values end @@ -124,7 +126,7 @@ module ActionDispatch # Returns the session as Hash. def to_hash load_for_read! - @delegate.dup.delete_if { |_,v| v.nil? } + @delegate.dup.delete_if { |_, v| v.nil? } end # Updates the session with given Hash. @@ -162,7 +164,7 @@ module ActionDispatch # :bar # end # # => :bar - def fetch(key, default=Unspecified, &block) + def fetch(key, default = Unspecified, &block) load_for_read! if default == Unspecified @delegate.fetch(key.to_s, &block) diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 282bdbd2be..3615e4b1d8 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -4,6 +4,17 @@ module ActionDispatch mattr_accessor :perform_deep_munge self.perform_deep_munge = true + def self.each_param_value(params, &block) + case params + when Array + params.each { |element| each_param_value(element, &block) } + when Hash + params.each_value { |value| each_param_value(value, &block) } + when String + block.call params + end + end + def self.normalize_encode_params(params) if perform_deep_munge NoNilParamEncoder.normalize_encode_params params @@ -29,7 +40,6 @@ module ActionDispatch class ParamEncoder # :nodoc: # Convert nested Hash to HashWithIndifferentAccess. - # def self.normalize_encode_params(params) case params when Array @@ -52,7 +62,7 @@ module ActionDispatch end end - # Remove nils from the params hash + # Remove nils from the params hash. class NoNilParamEncoder < ParamEncoder # :nodoc: def self.handle_array(params) list = super diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 61ebd0b8db..87dd1eba38 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/filters" + module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to # redirect incoming requests to controllers and actions. This replaces @@ -118,7 +120,7 @@ module ActionDispatch # controller :blog do # get 'blog/show' => :list # get 'blog/delete' => :delete - # get 'blog/edit' => :edit + # get 'blog/edit' => :edit # end # # # provides named routes for show, delete, and edit @@ -252,14 +254,5 @@ module ActionDispatch SEPARATORS = %w( / . ? ) #:nodoc: HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc: - - #:stopdoc: - INSECURE_URL_PARAMETERS_MESSAGE = <<-MSG.squish - Attempting to generate a URL from non-sanitized request parameters! - - An attacker can inject malicious data into the generated URL, such as - changing the host. Whitelist and sanitize passed parameters to be secure. - MSG - #:startdoc: end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index b91ffb8419..9aa4b92df2 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -196,7 +196,7 @@ module ActionDispatch @buffer << @view.render(partial: "routes/route", collection: routes) end - # the header is part of the HTML page, so we don't construct it here. + # The header is part of the HTML page, so we don't construct it here. def header(routes) end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 4ec1b8ee1f..74904e3d45 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -17,9 +17,9 @@ module ActionDispatch CALL = ->(app, req) { app.call req.env } def initialize(app, constraints, strategy) - # Unwrap Constraints objects. I don't actually think it's possible + # Unwrap Constraints objects. I don't actually think it's possible # to pass a Constraints object to this constructor, but there were - # multiple places that kept testing children of this object. I + # multiple places that kept testing children of this object. I # *think* they were just being defensive, but I have no idea. if app.is_a?(self.class) constraints += app.constraints @@ -54,6 +54,7 @@ module ActionDispatch class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z} attr_reader :requirements, :defaults attr_reader :to, :default_controller, :default_action @@ -93,7 +94,7 @@ module ActionDispatch end def self.optional_format?(path, format) - format != false && !path.include?(":format") && !path.end_with?("/") + format != false && path !~ OPTIONAL_FORMAT_REGEX end def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) @@ -218,7 +219,7 @@ module ActionDispatch private def add_wildcard_options(options, formatted, path_ast) # Add a constraint for wildcard route to make it non-greedy and match the - # optional format part of the route by default + # optional format part of the route by default. if formatted != false path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash| hash[node.name.to_sym] ||= /.+?/ @@ -239,7 +240,7 @@ module ActionDispatch options[:controller] ||= /.+?/ end - if to.respond_to? :call + if to.respond_to?(:action) || to.respond_to?(:call) options else to_endpoint = split_to to @@ -291,16 +292,14 @@ module ActionDispatch end def app(blocks) - if to.is_a?(Class) && to < ActionController::Metal + if to.respond_to?(:action) Routing::RouteSet::StaticDispatcher.new to + elsif to.respond_to?(:call) + Constraints.new(to, blocks, Constraints::CALL) + elsif blocks.any? + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) else - if to.respond_to?(:call) - Constraints.new(to, blocks, Constraints::CALL) - elsif blocks.any? - Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) - else - dispatcher(defaults.key?(:controller)) - end + dispatcher(defaults.key?(:controller)) end end @@ -398,7 +397,7 @@ module ActionDispatch end module Base - # Matches a url pattern to one or more routes. + # Matches a URL pattern to one or more routes. # # You should not use the +match+ method in your router # without specifying an HTTP method. @@ -408,7 +407,7 @@ module ActionDispatch # # sets :controller, :action and :id in params # match ':controller/:action/:id', via: [:get, :post] # - # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # Note that +:controller+, +:action+ and +:id+ are interpreted as URL # query parameters and thus available through +params+ in an action. # # If you want to expose your action to GET, use +get+ in the router: @@ -457,7 +456,7 @@ module ActionDispatch # # === Options # - # Any options not seen here are passed on as params with the url. + # Any options not seen here are passed on as params with the URL. # # [:controller] # The route's controller. @@ -568,7 +567,7 @@ module ActionDispatch # [:format] # Allows you to specify the default value for optional +format+ # segment or disable it by supplying +false+. - def match(path, options=nil) + def match(path, options = nil) end # Mount a Rack-based application to be used within the application. @@ -662,7 +661,7 @@ module ActionDispatch else prefix_options = options.slice(*_route.segment_keys) prefix_options[:relative_url_root] = "".freeze - # we must actually delete prefix segment keys to avoid passing them to next url_for + # We must actually delete prefix segment keys to avoid passing them to next url_for. _route.segment_keys.each { |k| options.delete(k) } _routes.url_helpers.send("#{name}_path", prefix_options) end @@ -997,65 +996,65 @@ module ActionDispatch end private - def merge_path_scope(parent, child) #:nodoc: + def merge_path_scope(parent, child) Mapper.normalize_path("#{parent}/#{child}") end - def merge_shallow_path_scope(parent, child) #:nodoc: + def merge_shallow_path_scope(parent, child) Mapper.normalize_path("#{parent}/#{child}") end - def merge_as_scope(parent, child) #:nodoc: + def merge_as_scope(parent, child) parent ? "#{parent}_#{child}" : child end - def merge_shallow_prefix_scope(parent, child) #:nodoc: + def merge_shallow_prefix_scope(parent, child) parent ? "#{parent}_#{child}" : child end - def merge_module_scope(parent, child) #:nodoc: + def merge_module_scope(parent, child) parent ? "#{parent}/#{child}" : child end - def merge_controller_scope(parent, child) #:nodoc: + def merge_controller_scope(parent, child) child end - def merge_action_scope(parent, child) #:nodoc: + def merge_action_scope(parent, child) child end - def merge_via_scope(parent, child) #:nodoc: + def merge_via_scope(parent, child) child end - def merge_format_scope(parent, child) #:nodoc: + def merge_format_scope(parent, child) child end - def merge_path_names_scope(parent, child) #:nodoc: + def merge_path_names_scope(parent, child) merge_options_scope(parent, child) end - def merge_constraints_scope(parent, child) #:nodoc: + def merge_constraints_scope(parent, child) merge_options_scope(parent, child) end - def merge_defaults_scope(parent, child) #:nodoc: + def merge_defaults_scope(parent, child) merge_options_scope(parent, child) end - def merge_blocks_scope(parent, child) #:nodoc: + def merge_blocks_scope(parent, child) merged = parent ? parent.dup : [] merged << child if child merged end - def merge_options_scope(parent, child) #:nodoc: + def merge_options_scope(parent, child) (parent || {}).merge(child) end - def merge_shallow_scope(parent, child) #:nodoc: + def merge_shallow_scope(parent, child) child ? true : false end @@ -1240,16 +1239,16 @@ module ActionDispatch # # resource :profile # - # creates six different routes in your application, all mapping to + # This creates six different routes in your application, all mapping to # the +Profiles+ controller (note that the controller is named after # the plural): # # GET /profile/new - # POST /profile # GET /profile # GET /profile/edit # PATCH/PUT /profile # DELETE /profile + # POST /profile # # === Options # Takes same options as +resources+. @@ -1267,15 +1266,15 @@ module ActionDispatch concerns(options[:concerns]) if options[:concerns] - collection do - post :create - end if parent_resource.actions.include?(:create) - new do get :new end if parent_resource.actions.include?(:new) set_member_mappings_for_resource + + collection do + post :create + end if parent_resource.actions.include?(:create) end end @@ -1325,14 +1324,14 @@ module ActionDispatch # # resources :posts, path_names: { new: "brand_new" } # - # The above example will now change /posts/new to /posts/brand_new + # The above example will now change /posts/new to /posts/brand_new. # # [:path] # Allows you to change the path prefix for the resource. # # resources :posts, path: 'postings' # - # The resource and all segments will now route to /postings instead of /posts + # The resource and all segments will now route to /postings instead of /posts. # # [:only] # Only generate routes for the given actions. @@ -1527,7 +1526,7 @@ module ActionDispatch end end - # See ActionDispatch::Routing::Mapper::Scoping#namespace + # See ActionDispatch::Routing::Mapper::Scoping#namespace. def namespace(path, options = {}) if resource_scope? nested { super } @@ -1547,7 +1546,7 @@ module ActionDispatch !parent_resource.singleton? && @scope[:shallow] end - # Matches a url pattern to one or more routes. + # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # # match 'path' => 'controller#action', via: patch @@ -1558,11 +1557,7 @@ module ActionDispatch options = path path, to = options.find { |name, _value| name.is_a?(String) } - if path.nil? - ActiveSupport::Deprecation.warn "Omitting the route path is deprecated. "\ - "Specify the path with a String or a Symbol instead." - path = "" - end + raise ArgumentError, "Route path not specified" if path.nil? case to when Symbol @@ -1624,13 +1619,13 @@ module ActionDispatch end end - protected + private - def parent_resource #:nodoc: + def parent_resource @scope[:scope_level_resource] end - def apply_common_behavior_for(method, resources, options, &block) #:nodoc: + def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true @@ -1663,39 +1658,39 @@ module ActionDispatch false end - def apply_action_options(options) # :nodoc: + def apply_action_options(options) return options if action_options? options options.merge scope_action_options end - def action_options?(options) #:nodoc: + def action_options?(options) options[:only] || options[:except] end - def scope_action_options #:nodoc: + def scope_action_options @scope[:action_options] || {} end - def resource_scope? #:nodoc: + def resource_scope? @scope.resource_scope? end - def resource_method_scope? #:nodoc: + def resource_method_scope? @scope.resource_method_scope? end - def nested_scope? #:nodoc: + def nested_scope? @scope.nested? end - def with_scope_level(kind) + def with_scope_level(kind) # :doc: @scope = @scope.new_level(kind) yield ensure @scope = @scope.parent end - def resource_scope(resource) #:nodoc: + def resource_scope(resource) @scope = @scope.new(scope_level_resource: resource) controller(resource.resource_scope) { yield } @@ -1703,7 +1698,7 @@ module ActionDispatch @scope = @scope.parent end - def nested_options #:nodoc: + def nested_options options = { as: parent_resource.member_name } options[:constraints] = { parent_resource.nested_param => param_constraint @@ -1712,25 +1707,25 @@ module ActionDispatch options end - def shallow_nesting_depth #:nodoc: + def shallow_nesting_depth @scope.find_all { |node| node.frame[:scope_level_resource] }.count { |node| node.frame[:scope_level_resource].shallow? } end - def param_constraint? #:nodoc: + def param_constraint? @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) end - def param_constraint #:nodoc: + def param_constraint @scope[:constraints][parent_resource.param] end - def canonical_action?(action) #:nodoc: + def canonical_action?(action) resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope #:nodoc: + def shallow_scope scope = { as: @scope[:shallow_prefix], path: @scope[:shallow_path] } @scope = @scope.new scope @@ -1740,7 +1735,7 @@ module ActionDispatch @scope = @scope.parent end - def path_for_action(action, path) #:nodoc: + def path_for_action(action, path) return "#{@scope[:path]}/#{path}" if path if canonical_action?(action) @@ -1750,11 +1745,11 @@ module ActionDispatch end end - def action_path(name) #:nodoc: + def action_path(name) @scope[:path_names][name.to_sym] || name end - def prefix_name_for_action(as, action) #:nodoc: + def prefix_name_for_action(as, action) if as prefix = as elsif !canonical_action?(action) @@ -1766,7 +1761,7 @@ module ActionDispatch end end - def name_for_action(as, action) #:nodoc: + def name_for_action(as, action) prefix = prefix_name_for_action(as, action) name_prefix = @scope[:as] @@ -1792,7 +1787,7 @@ module ActionDispatch end end - def set_member_mappings_for_resource + def set_member_mappings_for_resource # :doc: member do get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) @@ -1804,12 +1799,10 @@ module ActionDispatch end end - def api_only? + def api_only? # :doc: @set.api_only? end - private - def path_scope(path) @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) yield @@ -1844,18 +1837,7 @@ module ActionDispatch path_types.fetch(String, []).each do |_path| route_options = options.dup if _path && option_path - ActiveSupport::Deprecation.warn <<-eowarn -Specifying strings for both :path and the route path is deprecated. Change things like this: - - match #{_path.inspect}, :path => #{option_path.inspect} - -to this: - - match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{_path.inspect} - eowarn - route_options[:action] = _path - route_options[:as] = _path - _path = option_path + raise ArgumentError, "Ambigous route definition. Both :path and the route path where specified as strings." end to = get_to_from_path(_path, to, route_options[:action]) decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints) @@ -1884,7 +1866,7 @@ to this: path =~ %r{^/?[-\w]+/[-\w/]+$} end - def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) if on = options.delete(:on) send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else @@ -1899,7 +1881,7 @@ to this: end end - def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) path = path_for_action(action, _path) raise ArgumentError, "path is required" if path.blank? @@ -1923,7 +1905,7 @@ to this: ast = Journey::Parser.parse path mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) - @set.add_route(mapping, ast, as, anchor) + @set.add_route(mapping, as) end def match_root_route(options) @@ -2022,7 +2004,7 @@ to this: # concerns :commentable # end # - # concerns also work in any routes helper that you want to use: + # Concerns also work in any routes helper that you want to use: # # namespace :posts do # concerns :commentable @@ -2039,6 +2021,120 @@ to this: end end + module CustomUrls + # 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: + # + # direct :homepage do + # "http://www.rubyonrails.org" + # end + # + # direct :commentable do |model| + # [ model, anchor: model.dom_id ] + # end + # + # direct :main do + # { controller: "pages", action: "index", subdomain: "www" } + # end + # + # The return value from the block passed to `direct` must be a valid set of + # 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 + # + # 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. + # + # You can also specify default options that will be passed through to + # your URL helper definition, e.g: + # + # direct :browse, page: 1, size: 10 do |options| + # [ :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 + # 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: + # + # Rails.application.routes.url_helpers.browse_path + # + # then it will raise a `NameError`. Because of this you need to be aware of the + # context in which you will use your custom URL helper when defining it. + # + # NOTE: The `direct` method can't be used inside of a scope block such as + # `namespace` or `scope` and will raise an error if it detects that it is. + def direct(name, options = {}, &block) + unless @scope.root? + raise RuntimeError, "The direct method can't be used inside a routes scope block" + end + + @set.add_url_helper(name, options, &block) + end + + # Define custom polymorphic mappings of models to URLs. This alters the + # behavior of `polymorphic_url` and consequently the behavior of + # `link_to` and `form_for` when passed a model instance, e.g: + # + # resource :basket + # + # resolve "Basket" do + # [:basket] + # end + # + # This will now generate "/basket" when a `Basket` instance is passed to + # `link_to` or `form_for` instead of the standard "/baskets/:id". + # + # NOTE: This custom behavior only applies to simple polymorphic URLs where + # a single model instance is passed and not more complicated forms, e.g: + # + # # config/routes.rb + # resource :profile + # namespace :admin do + # resources :users + # end + # + # resolve("User") { [:profile] } + # + # # app/views/application/_menu.html.erb + # link_to "Profile", @current_user + # link_to "Profile", [:admin, @current_user] + # + # The first `link_to` will generate "/profile" but the second will generate + # the standard polymorphic URL of "/admin/users/1". + # + # You can pass options to a polymorphic mapping - the arity for the block + # needs to be two as the instance is passed as the first argument, e.g: + # + # resolve "Basket", anchor: "items" do |basket, options| + # [:basket, options] + # end + # + # This generates the URL "/basket#items" because when the last item in an + # array passed to `polymorphic_url` is a hash then it's treated as options + # to the URL helper that gets called. + # + # NOTE: The `resolve` method can't be used inside of a scope block such as + # `namespace` or `scope` and will raise an error if it detects that it is. + def resolve(*args, &block) + unless @scope.root? + raise RuntimeError, "The resolve method can't be used inside a routes scope block" + end + + options = args.extract_options! + args = args.flatten(1) + + args.each do |klass| + @set.add_polymorphic_mapping(klass, options, &block) + end + end + end + class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, @@ -2059,6 +2155,14 @@ to this: scope_level == :nested end + def null? + @hash.nil? && @parent.nil? + end + + def root? + @parent.null? + end + def resources? scope_level == :resources end @@ -2132,6 +2236,7 @@ to this: include Scoping include Concerns include Resources + include CustomUrls end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 4f1aaeefc8..e89ea8b21d 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -40,7 +40,7 @@ module ActionDispatch # # Example usage: # - # edit_polymorphic_path(@post) # => "/posts/1/edit" + # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf" # # == Usage with mounted engines @@ -103,6 +103,10 @@ module ActionDispatch return polymorphic_url record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array, options], false) + end + opts = options.dup action = opts.delete :action type = opts.delete(:routing_type) || :url @@ -123,6 +127,10 @@ module ActionDispatch return polymorphic_path record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array, options], true) + end + opts = options.dup action = opts.delete :action type = :path @@ -156,11 +164,19 @@ module ActionDispatch polymorphic_path(record_or_hash, options.merge(action: action)) end + def polymorphic_mapping(record) + if record.respond_to?(:to_model) + _routes.polymorphic_mappings[record.to_model.model_name.name] + else + _routes.polymorphic_mappings[record.class.name] + end + end + class HelperMethodBuilder # :nodoc: CACHE = { "path" => {}, "url" => {} } def self.get(action, type) - type = type.to_s + type = type.to_s CACHE[type].fetch(action) { build action, type } end @@ -255,9 +271,13 @@ module ActionDispatch [named_route, args] end - def handle_model_call(target, model) - method, args = handle_model model - target.send(method, *args) + def handle_model_call(target, record) + if mapping = polymorphic_mapping(target, record) + mapping.call(target, [record], suffix == "path") + else + method, args = handle_model(record) + target.send(method, *args) + end end def handle_list(list) @@ -266,7 +286,7 @@ module ActionDispatch args = [] - route = record_list.map { |parent| + route = record_list.map { |parent| case parent when Symbol, String parent.to_s @@ -303,8 +323,16 @@ module ActionDispatch private + def polymorphic_mapping(target, record) + if record.respond_to?(:to_model) + target._routes.polymorphic_mappings[record.to_model.model_name.name] + else + target._routes.polymorphic_mappings[record.class.name] + end + end + def get_method_for_class(klass) - name = @key_strategy.call klass.model_name + name = @key_strategy.call klass.model_name get_method_for_string name end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 87bcceccc0..3bcb341758 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -36,6 +36,8 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? + req.commit_flash + body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) headers = { @@ -61,15 +63,15 @@ module ActionDispatch end def escape(params) - Hash[params.map { |k,v| [k, Rack::Utils.escape(v)] }] + Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }] end def escape_fragment(params) - Hash[params.map { |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }] + Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }] end def escape_path(params) - Hash[params.map { |k,v| [k, Journey::Router::Utils.escape_path(v)] }] + Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }] end end @@ -128,7 +130,7 @@ module ActionDispatch end def inspect - "redirect(#{status}, #{options.map { |k,v| "#{k}: #{v}" }.join(', ')})" + "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})" end end @@ -137,11 +139,14 @@ module ActionDispatch # # get "/stories" => redirect("/posts") # + # This will redirect the user, while ignoring certain parts of the request, including query string, etc. + # `/stories`, `/stories?foo=bar`, etc all redirect to `/posts`. + # # You can also use interpolation in the supplied redirect argument: # # get 'docs/:article', to: redirect('/wiki/%{article}') # - # Note that if you return a path without a leading slash then the url is prefixed with the + # Note that if you return a path without a leading slash then the URL is prefixed with the # current SCRIPT_NAME environment variable. This is typically '/' but may be different in # a mounted engine or where the application is deployed to a subdirectory of a website. # @@ -160,11 +165,16 @@ module ActionDispatch # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead. # - # The options version of redirect allows you to supply only the parts of the url which need + # The options version of redirect allows you to supply only the parts of the URL which need # to change, it also supports interpolation of the path similar to the first example. # # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}') # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}') + # get '/stories', to: redirect(path: '/posts') + # + # This will redirect the user, while changing only the specified parts of the request, + # for example the `path` option in the last example. + # `/stories`, `/stories?foo=bar`, redirect to `/posts` and `/posts?foo=bar` respectively. # # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse # common redirect routes. The call method must accept two arguments, params and request, and return diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5abf59402d..e1f9fc9ecc 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,5 +1,4 @@ require "action_dispatch/journey" -require "active_support/concern" require "active_support/core_ext/object/to_query" require "active_support/core_ext/hash/slice" require "active_support/core_ext/module/remove_method" @@ -71,9 +70,10 @@ module ActionDispatch private :routes def initialize - @routes = {} + @routes = {} @path_helpers = Set.new @url_helpers = Set.new + @custom_helpers = Set.new @url_helpers_module = Module.new @path_helpers_module = Module.new end @@ -89,16 +89,30 @@ module ActionDispatch def clear! @path_helpers.each do |helper| - @path_helpers_module.send :undef_method, helper + @path_helpers_module.send :remove_method, helper end @url_helpers.each do |helper| - @url_helpers_module.send :undef_method, helper + @url_helpers_module.send :remove_method, helper + end + + @custom_helpers.each do |helper| + path_name = :"#{helper}_path" + url_name = :"#{helper}_url" + + if @path_helpers_module.method_defined?(path_name) + @path_helpers_module.send :remove_method, path_name + end + + if @url_helpers_module.method_defined?(url_name) + @url_helpers_module.send :remove_method, url_name + end end @routes.clear @path_helpers.clear @url_helpers.clear + @custom_helpers.clear end def add(name, route) @@ -144,6 +158,23 @@ module ActionDispatch routes.length end + def add_url_helper(name, defaults, &block) + @custom_helpers << name + helper = CustomUrlHelper.new(name, defaults, &block) + + @path_helpers_module.module_eval do + define_method(:"#{name}_path") do |*args| + helper.call(self, args, true) + end + end + + @url_helpers_module.module_eval do + define_method(:"#{name}_url") do |*args| + helper.call(self, args, false) + end + end + end + class UrlHelper def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) @@ -208,9 +239,9 @@ module ActionDispatch params = parameterize_args(args) { |missing_key| missing_keys << missing_key } - constraints = Hash[@route.requirements.merge(params).sort_by { |k,v| k.to_s }] + constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }] message = "No route matches #{constraints.inspect}" - message << " missing required keys: #{missing_keys.sort.inspect}" + message << ", missing required keys: #{missing_keys.sort.inspect}" raise ActionController::UrlGenerationError, message end @@ -264,7 +295,7 @@ module ActionDispatch end private - # Create a url helper allowing ordered parameters to be associated + # Create a URL helper allowing ordered parameters to be associated # with corresponding dynamic segments, so you can do: # # foo_url(bar, baz, bang) @@ -287,11 +318,7 @@ module ActionDispatch when Hash args.pop when ActionController::Parameters - if last.permitted? - args.pop.to_h - else - raise ArgumentError, ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE - end + args.pop.to_h end helper.call self, args, options end @@ -306,7 +333,7 @@ module ActionDispatch attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options - attr_reader :env_key + attr_reader :env_key, :polymorphic_mappings alias :routes :set @@ -348,6 +375,13 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set @formatter = Journey::Formatter.new self + @polymorphic_mappings = {} + end + + def eager_load! + router.eager_load! + routes.each(&:eager_load!) + nil end def relative_url_root @@ -403,6 +437,7 @@ module ActionDispatch named_routes.clear set.clear formatter.clear + @polymorphic_mappings.clear @prepend.each { |blk| eval_block(blk) } end @@ -447,17 +482,50 @@ module ActionDispatch # Define url_for in the singleton level so one can do: # Rails.application.routes.url_helpers.url_for(args) - @_routes = routes + proxy_class = Class.new do + include UrlFor + include routes.named_routes.path_helpers_module + include routes.named_routes.url_helpers_module + + attr_reader :_routes + + def initialize(routes) + @_routes = routes + end + + def optimize_routes_generation? + @_routes.optimize_routes_generation? + end + end + + @_proxy = proxy_class.new(routes) + class << self def url_for(options) - @_routes.url_for(options) + @_proxy.url_for(options) + end + + def full_url_for(options) + @_proxy.full_url_for(options) + end + + def route_for(name, *args) + @_proxy.route_for(name, *args) end def optimize_routes_generation? - @_routes.optimize_routes_generation? + @_proxy.optimize_routes_generation? end - attr_reader :_routes + def polymorphic_url(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_url(record_or_hash_or_array, options) + end + + def polymorphic_path(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_path(record_or_hash_or_array, options) + end + + def _routes; @_proxy._routes; end def url_options; {}; end end @@ -501,7 +569,7 @@ module ActionDispatch routes.empty? end - def add_route(mapping, path_ast, name, anchor) + def add_route(mapping, name) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] @@ -518,20 +586,58 @@ module ActionDispatch if route.segment_keys.include?(:controller) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :controller segment in a route is deprecated and - will be removed in Rails 5.1. + will be removed in Rails 5.2. MSG end if route.segment_keys.include?(:action) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :action segment in a route is deprecated and - will be removed in Rails 5.1. + will be removed in Rails 5.2. MSG end route end + def add_polymorphic_mapping(klass, options, &block) + @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block) + end + + def add_url_helper(name, options, &block) + named_routes.add_url_helper(name, options, &block) + end + + class CustomUrlHelper + attr_reader :name, :defaults, :block + + def initialize(name, defaults, &block) + @name = name + @defaults = defaults + @block = block + end + + def call(t, args, only_path = false) + options = args.extract_options! + url = t.full_url_for(eval_block(t, args, options)) + + if only_path + "/" + url.partition(%r{(?<!/)/(?!/)}).last + else + url + end + end + + private + def eval_block(t, args, options) + t.instance_exec(*args, merge_defaults(options), &block) + end + + def merge_defaults(options) + defaults ? defaults.merge(options) : options + end + end + class Generator PARAMETERIZE = lambda do |name, value| if name == :controller @@ -647,11 +753,11 @@ module ActionDispatch # Generate the path indicated by the arguments, and return an array of # the keys that were not used to generate it. - def extra_keys(options, recall={}) + def extra_keys(options, recall = {}) generate_extras(options, recall).last end - def generate_extras(options, recall={}) + def generate_extras(options, recall = {}) route_key = options.delete :use_route path, params = generate(route_key, options, recall) return path, params.keys @@ -693,7 +799,7 @@ module ActionDispatch password = options.delete :password end - recall = options.delete(:_recall) { {} } + recall = options.delete(:_recall) { {} } original_script_name = options.delete(:original_script_name) script_name = find_script_name options @@ -746,8 +852,7 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - old_params = req.path_parameters - req.path_parameters = old_params.merge params + req.path_parameters = params app = route.app if app.matches?(req) && app.dispatcher? begin diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index ee847eaeed..c1423f770f 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -19,7 +19,8 @@ module ActionDispatch end end - def respond_to_missing?(method, include_private = false) + private + def respond_to_missing?(method, _) super || @helpers.respond_to?(method) end @@ -32,7 +33,7 @@ module ActionDispatch @helpers.#{method}(*args) end RUBY - send(method, *args) + public_send(method, *args) else super end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index a1ac5a2b6c..a9bdefa775 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -113,10 +113,10 @@ module ActionDispatch default_url_options end - # Generate a url based on the options provided, default_url_options and the + # Generate a URL based on the options provided, default_url_options and the # routes defined in routes.rb. The following options are supported: # - # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. + # * <tt>:only_path</tt> - If true, the relative URL is returned. Defaults to +false+. # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. # * <tt>:host</tt> - Specifies the host the link should be targeted at. # If <tt>:only_path</tt> is false, this option must be @@ -164,20 +164,17 @@ module ActionDispatch # implicitly used by +url_for+ can always be overwritten like shown on the # last +url_for+ calls. def url_for(options = nil) + full_url_for(options) + end + + def full_url_for(options = nil) # :nodoc: case options when nil _routes.url_for(url_options.symbolize_keys) - when Hash - route_name = options.delete :use_route - _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), - route_name) - when ActionController::Parameters - unless options.permitted? - raise ArgumentError.new(ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE) - end + when Hash, ActionController::Parameters route_name = options.delete :use_route - _routes.url_for(options.to_h.symbolize_keys. - reverse_merge!(url_options), route_name) + merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options) + _routes.url_for(merged_url_options, route_name) when String options when Symbol @@ -192,20 +189,26 @@ module ActionDispatch end end + def route_for(name, *args) # :nodoc: + public_send(:"#{name}_url", *args) + end + protected def optimize_routes_generation? _routes.optimize_routes_generation? && default_url_options.empty? end - def _with_routes(routes) + private + + def _with_routes(routes) # :doc: old_routes, @_routes = @_routes, routes yield ensure @_routes = old_routes end - def _routes_context + def _routes_context # :doc: self end end diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb new file mode 100644 index 0000000000..98fdb36c91 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -0,0 +1,125 @@ +require "capybara/dsl" +require "capybara/minitest" +require "action_controller" +require "action_dispatch/system_testing/driver" +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" + +module ActionDispatch + # = System Testing + # + # System tests let you test applications in the browser. Because system + # tests use a real browser experience, you can test all of your JavaScript + # easily from your test suite. + # + # To create a system test in your application, extend your test class + # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a + # base and allow you to configure the settings through your + # <tt>application_system_test_case.rb</tt> file that is generated with a new + # application or scaffold. + # + # Here is an example system test: + # + # require 'application_system_test_case' + # + # class Users::CreateTest < ApplicationSystemTestCase + # test "adding a new user" do + # visit users_path + # click_on 'New User' + # + # fill_in 'Name', with: 'Arya' + # click_on 'Create User' + # + # assert_text 'Arya' + # end + # end + # + # When generating an application or scaffold, an +application_system_test_case.rb+ + # file will also be generated containing the base class for system testing. + # This is where you can change the driver, add Capybara settings, and other + # configuration for your system tests. + # + # require "test_helper" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :chrome, screen_size: [1400, 1400] + # end + # + # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the + # Selenium driver, with the Chrome browser, and a browser size of 1400x1400. + # + # Changing the driver configuration options is easy. Let's say you want to use + # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+ + # file add the following: + # + # require "test_helper" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :firefox + # end + # + # +driven_by+ has a required argument for the driver name. The keyword + # arguments are +:using+ for the browser and +:screen_size+ to change the + # size of the browser screen. These two options are not applicable for + # headless drivers and will be silently ignored if passed. + # + # 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 the +:using+ + # option because the driver is headless. + # + # require "test_helper" + # require "capybara/poltergeist" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :poltergeist + # end + # + # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara + # and Rails, any driver that is supported by Capybara is supported by system + # tests as long as you include the required gems and files. + class SystemTestCase < IntegrationTest + include Capybara::DSL + include Capybara::Minitest::Assertions + include SystemTesting::TestHelpers::SetupAndTeardown + include SystemTesting::TestHelpers::ScreenshotHelper + + def initialize(*) # :nodoc: + super + self.class.driver.use + end + + def self.start_application # :nodoc: + Capybara.app = Rack::Builder.new do + map "/" do + run Rails.application + end + end + + SystemTesting::Server.new.run + end + + class_attribute :driver, instance_accessor: false + + # System Test configuration options + # + # The default settings are Selenium, using Chrome, with a screen size + # of 1400x1400. + # + # Examples: + # + # driven_by :poltergeist + # + # driven_by :selenium, using: :firefox + # + # driven_by :selenium, screen_size: [800, 800] + 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 + + driven_by :selenium + end + + SystemTestCase.start_application +end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb new file mode 100644 index 0000000000..5cf17883f7 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -0,0 +1,34 @@ +module ActionDispatch + module SystemTesting + class Driver # :nodoc: + def initialize(name, **options) + @name = name + @browser = options[:using] + @screen_size = options[:screen_size] + @options = options[:options] + end + + def use + register if selenium? + setup + end + + private + def selenium? + @name == :selenium + end + + def register + Capybara.register_driver @name do |app| + Capybara::Selenium::Driver.new(app, { browser: @browser }.merge(@options)).tap do |driver| + driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) + end + end + end + + def setup + Capybara.current_driver = @name + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb new file mode 100644 index 0000000000..4a214ef713 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/server.rb @@ -0,0 +1,32 @@ +require "rack/handler/puma" + +module ActionDispatch + module SystemTesting + class Server # :nodoc: + def run + register + setup + end + + private + def register + Capybara.register_server :rails_puma do |app, port, host| + Rack::Handler::Puma.run(app, Port: port, Threads: "0:1") + end + end + + def setup + set_server + set_port + end + + def set_server + Capybara.server = :rails_puma + end + + def set_port + Capybara.always_include_port = true + end + end + end +end 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 new file mode 100644 index 0000000000..859d68e475 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -0,0 +1,94 @@ +module ActionDispatch + module SystemTesting + module TestHelpers + # Screenshot helper for system testing. + module ScreenshotHelper + # Takes a screenshot of the current page in the browser. + # + # +take_screenshot+ can be used at any point in your system tests to take + # a screenshot of the current state. This can be useful for debugging or + # automating visual testing. + # + # The screenshot will be displayed in your console, if supported. + # + # 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 + # iTerm image protocol (http://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 format (http://buildkite.github.io/terminal/inline-images/). + def take_screenshot + save_image + puts display_image + end + + # Takes a screenshot of the current page in the browser if the test + # failed. + # + # +take_failed_screenshot+ is included in <tt>application_system_test_case.rb</tt> + # that is generated with the application. To take screenshots when a test + # fails add +take_failed_screenshot+ to the teardown block before clearing + # sessions. + def take_failed_screenshot + take_screenshot if failed? && supports_screenshot? + end + + private + def image_name + failed? ? "failures_#{method_name}" : method_name + end + + def image_path + "tmp/screenshots/#{image_name}.png" + end + + def save_image + page.save_screenshot(Rails.root.join(image_path)) + end + + def output_type + # 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" + + output_type + end + + def display_image + message = "[Screenshot]: #{image_path}\n" + + case output_type + when "artifact" + message << "\e]1338;url=artifact://#{image_path}\a\n" + when "inline" + name = inline_base64(File.basename(image_path)) + image = inline_base64(File.read(image_path)) + message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n" + end + + message + end + + def inline_base64(path) + Base64.encode64(path).gsub("\n", "") + end + + def failed? + !passed? && !skipped? + end + + def supports_screenshot? + Capybara.current_driver != :rack_test + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb new file mode 100644 index 0000000000..f03f0d4299 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb @@ -0,0 +1,25 @@ +module ActionDispatch + module SystemTesting + module TestHelpers + module SetupAndTeardown # :nodoc: + DEFAULT_HOST = "http://127.0.0.1" + + def host!(host) + super + Capybara.app_host = host + end + + def before_setup + host! DEFAULT_HOST + super + end + + def after_teardown + take_failed_screenshot + Capybara.reset_sessions! + super + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index b362931ef7..4ea18d671d 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type.to_s =~ /xml\z/ + @html_document ||= if @response.content_type.to_s.end_with?("xml") Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index a2eaccd9ef..1baf979ac9 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -45,12 +45,12 @@ module ActionDispatch # # Asserts that the redirection was to the named route login_url # assert_redirected_to login_url # - # # Asserts that the redirection was to the url for @customer + # # Asserts that the redirection was to the URL for @customer # assert_redirected_to @customer # # # Asserts that the redirection matches the regular expression # assert_redirected_to %r(\Ahttp://example.org) - def assert_redirected_to(options = {}, message=nil) + def assert_redirected_to(options = {}, message = nil) assert_response(:redirect, message) return true if options === @response.location diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index e53bc6af12..8645df4370 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -18,8 +18,8 @@ module ActionDispatch # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post}) # # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used - # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the - # extras argument, appending the query string on the path directly will not work. For example: + # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras + # argument because appending the query string on the path directly will not work. For example: # # # Asserts that a path of '/items/list/1?view=print' returns the correct options # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" }) @@ -37,7 +37,7 @@ module ActionDispatch # # # Test a custom route # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1') - def assert_recognizes(expected_options, path, extras={}, msg=nil) + def assert_recognizes(expected_options, path, extras = {}, msg = nil) if path.is_a?(Hash) && path[:method].to_s == "all" [:get, :post, :put, :delete].each do |method| assert_recognizes(expected_options, path.merge(method: method), extras, msg) @@ -75,7 +75,7 @@ module ActionDispatch # # # Asserts that the generated route gives us our custom route # assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" } - def assert_generates(expected_path, options, defaults={}, extras={}, message=nil) + def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil) if expected_path =~ %r{://} fail_on(URI::InvalidURIError, message) do uri = URI.parse(expected_path) @@ -119,7 +119,7 @@ module ActionDispatch # # # Tests a route with an HTTP method # assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" }) - def assert_routing(path, options, defaults={}, extras={}, message=nil) + def assert_routing(path, options, defaults = {}, extras = {}, message = nil) assert_recognizes(options, path, extras, message) controller, default_controller = options[:controller], defaults[:controller] @@ -132,8 +132,7 @@ module ActionDispatch end # A helper to make it easier to test different route configurations. - # This method temporarily replaces @routes - # with a new RouteSet instance. + # This method temporarily replaces @routes with a new RouteSet instance. # # The new instance is yielded to the passed block. Typically the block # will create some routes using <tt>set.draw { match ... }</tt>: @@ -152,8 +151,11 @@ module ActionDispatch _routes = @routes @controller.singleton_class.include(_routes.url_helpers) - @controller.view_context_class = Class.new(@controller.view_context_class) do - include _routes.url_helpers + + if @controller.respond_to? :view_context_class + @controller.view_context_class = Class.new(@controller.view_context_class) do + include _routes.url_helpers + end end end yield @routes @@ -183,7 +185,6 @@ module ActionDispatch method = :get end - # Assume given controller request = ActionController::TestRequest.create @controller.class if path =~ %r{://} diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 720651fa1f..2416c58817 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -2,7 +2,6 @@ require "stringio" require "uri" require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/object/try" -require "active_support/core_ext/string/strip" require "rack/test" require "minitest" @@ -11,99 +10,42 @@ require "action_dispatch/testing/request_encoder" module ActionDispatch module Integration #:nodoc: module RequestHelpers - # Performs a GET request with the given parameters. - # - # - +path+: The URI (as a String) on which you want to perform a GET - # request. - # - +params+: The HTTP parameters that you want to pass. This may - # be +nil+, - # a Hash, or a String that is appropriately encoded - # (<tt>application/x-www-form-urlencoded</tt> or - # <tt>multipart/form-data</tt>). - # - +headers+: Additional headers to pass, as a Hash. The headers will be - # merged into the Rack env hash. - # - +env+: Additional env to pass, as a Hash. The headers will be - # merged into the Rack env hash. - # - # This method returns a Response object, which one can use to - # inspect the details of the response. Furthermore, if this method was - # called from an ActionDispatch::IntegrationTest object, then that - # object's <tt>@response</tt> instance variable will point to the same - # response object. - # - # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with - # +#post+, +#patch+, +#put+, +#delete+, and +#head+. - # - # Example: - # - # get '/feed', params: { since: 201501011400 } - # post '/profile', headers: { "X-Test-Header" => "testvalue" } - def get(path, *args) - process_with_kwargs(:get, path, *args) + # Performs a GET request with the given parameters. See +#process+ for more + # details. + def get(path, **args) + process(:get, path, **args) end - # Performs a POST request with the given parameters. See +#get+ for more + # Performs a POST request with the given parameters. See +#process+ for more # details. - def post(path, *args) - process_with_kwargs(:post, path, *args) + def post(path, **args) + process(:post, path, **args) end - # Performs a PATCH request with the given parameters. See +#get+ for more + # Performs a PATCH request with the given parameters. See +#process+ for more # details. - def patch(path, *args) - process_with_kwargs(:patch, path, *args) + def patch(path, **args) + process(:patch, path, **args) end - # Performs a PUT request with the given parameters. See +#get+ for more + # Performs a PUT request with the given parameters. See +#process+ for more # details. - def put(path, *args) - process_with_kwargs(:put, path, *args) + def put(path, **args) + process(:put, path, **args) end - # Performs a DELETE request with the given parameters. See +#get+ for + # Performs a DELETE request with the given parameters. See +#process+ for # more details. - def delete(path, *args) - process_with_kwargs(:delete, path, *args) + def delete(path, **args) + process(:delete, path, **args) end - # Performs a HEAD request with the given parameters. See +#get+ for more + # Performs a HEAD request with the given parameters. See +#process+ for more # details. def head(path, *args) - process_with_kwargs(:head, path, *args) + process(:head, path, *args) end - # Performs an XMLHttpRequest request with the given parameters, mirroring - # an AJAX request made from JavaScript. - # - # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or - # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart - # string; the headers are a hash. - # - # Example: - # - # xhr :get, '/feed', params: { since: 201501011400 } - def xml_http_request(request_method, path, *args) - if kwarg_request?(args) - params, headers, env = args.first.values_at(:params, :headers, :env) - else - params = args[0] - headers = args[1] - env = {} - - if params.present? || headers.present? - non_kwarg_request_warning - end - end - - ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - xhr and xml_http_request methods are deprecated in favor of - `get "/posts", xhr: true` and `post "/posts/1", xhr: true`. - MSG - - process(request_method, path, params: params, headers: headers, xhr: true) - end - alias xhr :xml_http_request - # Follow a single redirect response. If the last response was not a # redirect, an exception will be raised. Otherwise, the redirect is # performed on the location header. @@ -112,59 +54,6 @@ module ActionDispatch get(response.location) status end - - # Performs a request using the specified method, following any subsequent - # redirect. Note that the redirects are followed until the response is - # not a redirect--this means you may run into an infinite loop if your - # redirect loops back to itself. - # - # Example: - # - # request_via_redirect :post, '/welcome', - # params: { ref_id: 14 }, - # headers: { "X-Test-Header" => "testvalue" } - def request_via_redirect(http_method, path, *args) - ActiveSupport::Deprecation.warn("`request_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.") - process_with_kwargs(http_method, path, *args) - - follow_redirect! while redirect? - status - end - - # Performs a GET request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def get_via_redirect(path, *args) - ActiveSupport::Deprecation.warn("`get_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.") - request_via_redirect(:get, path, *args) - end - - # Performs a POST request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def post_via_redirect(path, *args) - ActiveSupport::Deprecation.warn("`post_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.") - request_via_redirect(:post, path, *args) - end - - # Performs a PATCH request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def patch_via_redirect(path, *args) - ActiveSupport::Deprecation.warn("`patch_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.") - request_via_redirect(:patch, path, *args) - end - - # Performs a PUT request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def put_via_redirect(path, *args) - ActiveSupport::Deprecation.warn("`put_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.") - request_via_redirect(:put, path, *args) - end - - # Performs a DELETE request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def delete_via_redirect(path, *args) - ActiveSupport::Deprecation.warn("`delete_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.") - request_via_redirect(:delete, path, *args) - end end # An instance of this class represents a set of requests and responses @@ -179,7 +68,7 @@ module ActionDispatch DEFAULT_HOST = "www.example.com" include Minitest::Assertions - include RequestHelpers, Assertions + include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| delegate method, to: :response, allow_nil: true @@ -255,8 +144,8 @@ module ActionDispatch self.host = DEFAULT_HOST self.remote_addr = "127.0.0.1" - self.accept = "text/xml,application/xml,application/xhtml+xml," + - "text/html;q=0.9,text/plain;q=0.8,image/png," + + self.accept = "text/xml,application/xml,application/xhtml+xml," \ + "text/html;q=0.9,text/plain;q=0.8,image/png," \ "*/*;q=0.5" unless defined? @named_routes_configured @@ -283,132 +172,125 @@ module ActionDispatch @https end - # Set the host name to use in the next request. + # Performs the actual request. # - # session.host! "www.example.com" - alias :host! :host= - - private - def _mock_session - @_mock_session ||= Rack::MockSession.new(@app, host) - end - - def process_with_kwargs(http_method, path, *args) - if kwarg_request?(args) - process(http_method, path, *args) - else - non_kwarg_request_warning if args.any? - process(http_method, path, params: args[0], headers: args[1]) - end - end - - REQUEST_KWARGS = %i(params headers env xhr as) - def kwarg_request?(args) - args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } - end - - def non_kwarg_request_warning - ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - ActionDispatch::IntegrationTest HTTP request methods will accept only - the following keyword arguments in future Rails versions: - #{REQUEST_KWARGS.join(', ')} - - Examples: - - get '/profile', - params: { id: 1 }, - headers: { 'X-Extra-Header' => '123' }, - env: { 'action_dispatch.custom' => 'custom' }, - xhr: true, - as: :json - MSG + # - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS) + # as a symbol. + # - +path+: The URI (as a String) on which you want to perform the + # request. + # - +params+: The HTTP parameters that you want to pass. This may + # be +nil+, + # a Hash, or a String that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or + # <tt>multipart/form-data</tt>). + # - +headers+: Additional headers to pass, as a Hash. The headers will be + # merged into the Rack env hash. + # - +env+: Additional env to pass, as a Hash. The headers will be + # merged into the Rack env hash. + # + # This method is rarely used directly. Use +#get+, +#post+, or other standard + # HTTP methods in integration tests. +#process+ is only required when using a + # request method that doesn't have a method defined in the integration tests. + # + # This method returns the response status, after performing the request. + # Furthermore, if this method was called from an ActionDispatch::IntegrationTest object, + # then that object's <tt>@response</tt> instance variable will point to a Response object + # which one can use to inspect the details of the response. + # + # Example: + # process :get, '/author', params: { since: 201501011400 } + def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil) + request_encoder = RequestEncoder.encoder(as) + headers ||= {} + + if method == :get && as == :json && params + headers["X-Http-Method-Override"] = "GET" + method = :post end - # Performs the actual request. - def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil) - request_encoder = RequestEncoder.encoder(as) - headers ||= {} + if path =~ %r{://} + path = build_expanded_path(path) do |location| + https! URI::HTTPS === location if location.scheme - if method == :get && as == :json && params - headers["X-Http-Method-Override"] = "GET" - method = :post + if url_host = location.host + default = Rack::Request::DEFAULT_PORTS[location.scheme] + url_host += ":#{location.port}" if default != location.port + host! url_host + end end + end - if path =~ %r{://} - path = build_expanded_path(path, request_encoder) do |location| - https! URI::HTTPS === location if location.scheme + hostname, port = host.split(":") - if url_host = location.host - default = Rack::Request::DEFAULT_PORTS[location.scheme] - url_host += ":#{location.port}" if default != location.port - host! url_host - end - end - elsif as - path = build_expanded_path(path, request_encoder) - end + request_env = { + :method => method, + :params => request_encoder.encode_params(params), - hostname, port = host.split(":") + "SERVER_NAME" => hostname, + "SERVER_PORT" => port || (https? ? "443" : "80"), + "HTTPS" => https? ? "on" : "off", + "rack.url_scheme" => https? ? "https" : "http", - request_env = { - :method => method, - :params => request_encoder.encode_params(params), + "REQUEST_URI" => path, + "HTTP_HOST" => host, + "REMOTE_ADDR" => remote_addr, + "CONTENT_TYPE" => request_encoder.content_type, + "HTTP_ACCEPT" => request_encoder.accept_header || accept + } - "SERVER_NAME" => hostname, - "SERVER_PORT" => port || (https? ? "443" : "80"), - "HTTPS" => https? ? "on" : "off", - "rack.url_scheme" => https? ? "https" : "http", + wrapped_headers = Http::Headers.from_hash({}) + wrapped_headers.merge!(headers) if headers - "REQUEST_URI" => path, - "HTTP_HOST" => host, - "REMOTE_ADDR" => remote_addr, - "CONTENT_TYPE" => request_encoder.content_type, - "HTTP_ACCEPT" => accept - } + if xhr + wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" + wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ") + end - wrapped_headers = Http::Headers.from_hash({}) - wrapped_headers.merge!(headers) if headers + # This modifies the passed request_env directly. + if wrapped_headers.present? + Http::Headers.from_hash(request_env).merge!(wrapped_headers) + end + if env.present? + Http::Headers.from_hash(request_env).merge!(env) + end - if xhr - wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" - wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ") - end + session = Rack::Test::Session.new(_mock_session) - # this modifies the passed request_env directly - if wrapped_headers.present? - Http::Headers.from_hash(request_env).merge!(wrapped_headers) - end - if env.present? - Http::Headers.from_hash(request_env).merge!(env) - end + # NOTE: rack-test v0.5 doesn't build a default uri correctly + # Make sure requested path is always a full URI. + session.request(build_full_uri(path, request_env), request_env) - session = Rack::Test::Session.new(_mock_session) + @request_count += 1 + @request = ActionDispatch::Request.new(session.last_request.env) + response = _mock_session.last_response + @response = ActionDispatch::TestResponse.from_response(response) + @response.request = @request + @html_document = nil + @url_options = nil - # NOTE: rack-test v0.5 doesn't build a default uri correctly - # Make sure requested path is always a full uri - session.request(build_full_uri(path, request_env), request_env) + @controller = @request.controller_instance - @request_count += 1 - @request = ActionDispatch::Request.new(session.last_request.env) - response = _mock_session.last_response - @response = ActionDispatch::TestResponse.from_response(response) - @response.request = @request - @html_document = nil - @url_options = nil + response.status + end - @controller = @request.controller_instance + # Set the host name to use in the next request. + # + # session.host! "www.example.com" + alias :host! :host= - response.status + private + def _mock_session + @_mock_session ||= Rack::MockSession.new(@app, host) end def build_full_uri(path, env) "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" end - def build_expanded_path(path, request_encoder) + def build_expanded_path(path) location = URI.parse(path) yield location if block_given? - path = request_encoder.append_format_to location.path + path = location.path location.query ? "#{path}?#{location.query}" : path end end @@ -442,8 +324,8 @@ module ActionDispatch def create_session(app) klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) { - # If the app is a Rails app, make url_helpers available on the session - # This makes app.url_for and app.foo_path available in the console + # If the app is a Rails app, make url_helpers available on the session. + # This makes app.url_for and app.foo_path available in the console. if app.respond_to?(:routes) include app.routes.url_helpers include app.routes.mounted_helpers @@ -482,6 +364,7 @@ module ActionDispatch # simultaneously. def open_session dup.tap do |session| + session.reset! yield session if block_given? end end @@ -502,14 +385,15 @@ module ActionDispatch integration_session.default_url_options = options end - def respond_to_missing?(method, include_private = false) - integration_session.respond_to?(method, include_private) || super + private + def respond_to_missing?(method, _) + integration_session.respond_to?(method) || super end # Delegate unhandled messages to the current session instance. - def method_missing(sym, *args, &block) - if integration_session.respond_to?(sym) - integration_session.__send__(sym, *args, &block).tap do + def method_missing(method, *args, &block) + if integration_session.respond_to?(method) + integration_session.public_send(method, *args, &block).tap do copy_session_variables! end else @@ -688,17 +572,19 @@ module ActionDispatch # end # # assert_response :success - # assert_equal({ id: Arcticle.last.id, title: "Ahoy!" }, response.parsed_body) + # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body) # end # end # - # The +as+ option sets the format to JSON, sets the content type to + # The +as+ option passes an "application/json" Accept header (thereby setting + # the request format to JSON unless overridden), sets the content type to # "application/json" and encodes the parameters as JSON. # # Calling +parsed_body+ on the response parses the response body based on the # last response MIME type. # - # For any custom MIME types you've registered, you can even add your own encoders with: + # Out of the box, only <tt>:json</tt> is supported. But for any custom MIME + # types you've registered, you can add your own encoders with: # # ActionDispatch::IntegrationTest.register_encoder :wibble, # param_encoder: -> params { params.to_wibble }, @@ -711,7 +597,7 @@ module ActionDispatch # Consult the Rails Testing Guide for more. class IntegrationTest < ActiveSupport::TestCase - include TestProcess + include TestProcess::FixtureFile module UrlOptions extend ActiveSupport::Concern diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb index b0b994b2d0..8c27e9ecb7 100644 --- a/actionpack/lib/action_dispatch/testing/request_encoder.rb +++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb @@ -1,10 +1,17 @@ module ActionDispatch class RequestEncoder # :nodoc: - @encoders = {} + class IdentityEncoder + def content_type; end + def accept_header; end + def encode_params(params); params; end + def response_parser; -> body { body }; end + end + + @encoders = { identity: IdentityEncoder.new } attr_reader :response_parser - def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false) + def initialize(mime_name, param_encoder, response_parser) @mime = Mime[mime_name] unless @mime @@ -12,21 +19,15 @@ module ActionDispatch "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`." end - @url_encoded_form = url_encoded_form - @path_format = ".#{@mime.symbol}" unless @url_encoded_form - @response_parser = response_parser || -> body { body } - @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc + @response_parser = response_parser || -> body { body } + @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc end - def append_format_to(path) - if @url_encoded_form - path - else - path + @path_format - end + def content_type + @mime.to_s end - def content_type + def accept_header @mime.to_s end @@ -40,7 +41,7 @@ module ActionDispatch end def self.encoder(name) - @encoders[name] || WWWFormEncoder + @encoders[name] || @encoders[:identity] end def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil) @@ -48,7 +49,5 @@ module ActionDispatch end register_encoder :json, response_parser: -> body { JSON.parse(body) } - - WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true) end end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 8b03b776fa..0282eb15c3 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -3,6 +3,26 @@ require "action_dispatch/middleware/flash" module ActionDispatch module TestProcess + module FixtureFile + # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: + # + # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') + # + # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter. + # This will not affect other platforms: + # + # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary) + def fixture_file_upload(path, mime_type = nil, binary = false) + if self.class.respond_to?(:fixture_path) && self.class.fixture_path && + !File.exist?(path) + path = File.join(self.class.fixture_path, path) + end + Rack::Test::UploadedFile.new(path, mime_type, binary) + end + end + + include FixtureFile + def assigns(key = nil) raise NoMethodError, "assigns has been extracted to a gem. To continue using it, @@ -24,21 +44,5 @@ module ActionDispatch def redirect_to_url @response.redirect_url end - - # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: - # - # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') - # - # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter. - # This will not affect other platforms: - # - # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary) - def fixture_file_upload(path, mime_type = nil, binary = false) - if self.class.respond_to?(:fixture_path) && self.class.fixture_path && - !File.exist?(path) - path = File.join(self.class.fixture_path, path) - end - Rack::Test::UploadedFile.new(path, mime_type, binary) - end end end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 91b25ec155..ec949c869b 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -9,7 +9,7 @@ module ActionDispatch "HTTP_USER_AGENT" => "Rails Testing", ) - # Create a new test request with default `env` values + # Create a new test request with default `env` values. def self.create(env = {}) env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application env["rack.request.cookie_hash"] ||= {}.with_indifferent_access diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index ee6dabd133..eec622e085 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 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 d8f86630b1..fddc3033d5 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -6,7 +6,7 @@ module ActionPack module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index a0f2efa330..9c2261bf76 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -264,53 +264,5 @@ module AbstractController assert_equal "Hello world Howdy!", controller.response_body end end - - class AliasedCallbacks < ControllerWithCallbacks - ActiveSupport::Deprecation.silence do - before_filter :first - after_filter :second - around_filter :aroundz - end - - def first - @text = "Hello world" - end - - def second - @second = "Goodbye" - end - - def aroundz - @aroundz = "FIRST" - yield - @aroundz << "SECOND" - end - - def index - @text ||= nil - self.response_body = @text.to_s - end - end - - class TestAliasedCallbacks < ActiveSupport::TestCase - def setup - @controller = AliasedCallbacks.new - end - - test "before_filter works" do - @controller.process(:index) - assert_equal "Hello world", @controller.response_body - end - - test "after_filter works" do - @controller.process(:index) - assert_equal "Goodbye", @controller.instance_variable_get("@second") - end - - test "around_filter works" do - @controller.process(:index) - assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz") - end - end end end diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index 7fe19e6b10..1cd3526483 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -55,7 +55,7 @@ module AbstractController collector.js(:bar) { :baz } assert_equal [Mime[:html], [], nil], collector.responses[0] assert_equal [Mime[:text], [:foo], nil], collector.responses[1] - assert_equal [Mime[:js], [:bar]], collector.responses[2][0,2] + assert_equal [Mime[:js], [:bar]], collector.responses[2][0, 2] assert_equal :baz, collector.responses[2][2].call end end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 6d28753947..bd118b46be 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -1,14 +1,14 @@ -$:.unshift(File.dirname(__FILE__) + "/lib") -$:.unshift(File.dirname(__FILE__) + "/fixtures/helpers") -$:.unshift(File.dirname(__FILE__) + "/fixtures/alternate_helpers") +$:.unshift File.expand_path("lib", __dir__) +$:.unshift File.expand_path("fixtures/helpers", __dir__) +$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__) require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end require "drb" @@ -56,7 +56,7 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), "fixtures") +FIXTURE_LOAD_PATH = File.join(__dir__, "fixtures") SharedTestRoutes = ActionDispatch::Routing::RouteSet.new @@ -156,7 +156,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end def with_autoload_path(path) - path = File.join(File.dirname(__FILE__), "fixtures", path) + path = File.join(__dir__, "fixtures", path) if ActiveSupport::Dependencies.autoload_paths.include?(path) yield else @@ -259,9 +259,9 @@ module ActionDispatch host = uri_or_host.host unless path path ||= uri_or_host.path - params = { "PATH_INFO" => path, - "REQUEST_METHOD" => method, - "HTTP_HOST" => host } + params = { "PATH_INFO" => path, + "REQUEST_METHOD" => method, + "HTTP_HOST" => host } routes.call(params) end @@ -351,18 +351,9 @@ class ResourcesController < ActionController::Base end class CommentsController < ResourcesController; end -class AccountsController < ResourcesController; end +class AccountsController < ResourcesController; end class ImagesController < ResourcesController; end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - require "active_support/testing/method_call_assertions" class ForkingExecutor @@ -438,4 +429,21 @@ end class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end +end + +class DrivenByRackTest < ActionDispatch::SystemTestCase + driven_by :rack_test +end + +class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index b08f1f1707..73aab5848b 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -83,7 +83,7 @@ class ActionPackAssertionsController < ActionController::Base end def render_file_absolute_path - render file: File.expand_path("../../../README.rdoc", __FILE__) + render file: File.expand_path("../../README.rdoc", __dir__) end def render_file_relative_path @@ -128,6 +128,16 @@ module Admin end end +class ApiOnlyController < ActionController::API + def nothing + head :ok + end + + def redirect_to_new_route + redirect_to new_route_url + end +end + class ActionPackAssertionsControllerTest < ActionController::TestCase def test_render_file_absolute_path get :render_file_absolute_path @@ -170,6 +180,20 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end + def test_with_routing_works_with_api_only_controllers + @controller = ApiOnlyController.new + + with_routing do |set| + set.draw do + get "new_route", to: "api_only#nothing" + get "redirect_to_new_route", to: "api_only#redirect_to_new_route" + end + + process :redirect_to_new_route + assert_redirected_to "http://test.host/new_route" + end + end + def test_assert_redirect_to_named_route_failure with_routing do |set| set.draw do diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb index f15b78d102..e6419b9adf 100644 --- a/actionpack/test/controller/api/data_streaming_test.rb +++ b/actionpack/test/controller/api/data_streaming_test.rb @@ -1,7 +1,7 @@ require "abstract_unit" module TestApiFileUtils - def file_path() File.expand_path(__FILE__) end + def file_path() __FILE__ end def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end end diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb index 7eecc1c680..04e34a1f8f 100644 --- a/actionpack/test/controller/api/renderers_test.rb +++ b/actionpack/test/controller/api/renderers_test.rb @@ -23,10 +23,6 @@ class RenderersApiController < ActionController::API def plain render plain: "Hi from plain", status: 500 end - - def text - render text: "Hi from text", status: 500 - end end class RenderersApiTest < ActionController::TestCase @@ -49,12 +45,4 @@ class RenderersApiTest < ActionController::TestCase assert_response :internal_server_error assert_equal("Hi from plain", @response.body) end - - def test_render_text - assert_deprecated do - get :text - end - assert_response :internal_server_error - assert_equal("Hi from text", @response.body) - end end diff --git a/actionpack/test/controller/api/with_helpers_test.rb b/actionpack/test/controller/api/with_helpers_test.rb new file mode 100644 index 0000000000..06db949153 --- /dev/null +++ b/actionpack/test/controller/api/with_helpers_test.rb @@ -0,0 +1,42 @@ +require "abstract_unit" + +module ApiWithHelper + def my_helper + "helper" + end +end + +class WithHelpersController < ActionController::API + include ActionController::Helpers + helper ApiWithHelper + + def with_helpers + render plain: self.class.helpers.my_helper + end +end + +class SubclassWithHelpersController < WithHelpersController + def with_helpers + render plain: self.class.helpers.my_helper + end +end + +class WithHelpersTest < ActionController::TestCase + tests WithHelpersController + + def test_with_helpers + get :with_helpers + + assert_equal "helper", response.body + end +end + +class SubclassWithHelpersTest < ActionController::TestCase + tests WithHelpersController + + def test_with_helpers + get :with_helpers + + assert_equal "helper", response.body + end +end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 42a5157010..4e969fac07 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -11,6 +11,12 @@ end class EmptyController < ActionController::Base end +class SimpleController < ActionController::Base + def hello + self.response_body = "hello" + end +end + class NonEmptyController < ActionController::Base def public_action head :ok @@ -118,6 +124,27 @@ class ControllerInstanceTests < ActiveSupport::TestCase controller = klass.new assert_equal "examples", controller.controller_path end + + def test_response_has_default_headers + original_default_headers = ActionDispatch::Response.default_headers + + ActionDispatch::Response.default_headers = { + "X-Frame-Options" => "DENY", + "X-Content-Type-Options" => "nosniff", + "X-XSS-Protection" => "1;" + } + + response_headers = SimpleController.action("hello").call( + "REQUEST_METHOD" => "GET", + "rack.input" => -> {} + )[1] + + assert response_headers.key?("X-Frame-Options") + assert response_headers.key?("X-Content-Type-Options") + assert response_headers.key?("X-XSS-Protection") + ensure + ActionDispatch::Response.default_headers = original_default_headers + end end class PerformActionTest < ActionController::TestCase diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 18490c7d73..c86dcafee5 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -4,7 +4,7 @@ require "lib/controller/fake_models" CACHE_DIR = "test_cache" # Don't change '/../temp/' cavalierly or you might hose something you don't want hosed -FILE_STORE_PATH = File.join(File.dirname(__FILE__), "/../temp/", CACHE_DIR) +FILE_STORE_PATH = File.join(__dir__, "../temp/", CACHE_DIR) class FragmentCachingMetalTestController < ActionController::Metal abstract! @@ -26,10 +26,6 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller.request = @request @controller.response = @response end - - def test_fragment_cache_key - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - end end class CachingController < ActionController::Base @@ -43,6 +39,8 @@ class FragmentCachingTestController < CachingController end class FragmentCachingTest < ActionController::TestCase + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + def setup super @store = ActiveSupport::Cache::MemoryStore.new @@ -53,12 +51,25 @@ class FragmentCachingTest < ActionController::TestCase @controller.params = @params @controller.request = @request @controller.response = @response + + @m1v1 = ModelWithKeyAndVersion.new("model/1", "1") + @m1v2 = ModelWithKeyAndVersion.new("model/1", "2") + @m2v1 = ModelWithKeyAndVersion.new("model/2", "1") + @m2v2 = ModelWithKeyAndVersion.new("model/2", "2") end def test_fragment_cache_key - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - assert_equal "views/test.host/fragment_caching_test/some_action", - @controller.fragment_cache_key(controller: "fragment_caching_test",action: "some_action") + assert_deprecated do + assert_equal "views/what a key", @controller.fragment_cache_key("what a key") + assert_equal "views/test.host/fragment_caching_test/some_action", + @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + end + end + + def test_combined_fragment_cache_key + assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key") + assert_equal [ :views, "test.host/fragment_caching_test/some_action" ], + @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action") end def test_read_fragment_with_caching_enabled @@ -72,6 +83,12 @@ class FragmentCachingTest < ActionController::TestCase assert_nil @controller.read_fragment("name") end + def test_read_fragment_with_versioned_model + @controller.write_fragment([ "stuff", @m1v1 ], "hello") + assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ]) + assert_nil @controller.read_fragment([ "stuff", @m1v2 ]) + end + def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @controller.fragment_exist?("name") @@ -198,7 +215,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "This bit's fragment cached", - @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached")}") + @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment") end def test_fragment_caching_in_partials @@ -207,7 +224,7 @@ CACHED assert_match(/Old fragment caching in a partial/, @response.body) assert_match("Old fragment caching in a partial", - @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial")}")) + @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial")) end def test_skipping_fragment_cache_digesting @@ -237,7 +254,7 @@ CACHED assert_match(/Some inline content/, @response.body) assert_match(/Some cached content/, @response.body) assert_match("Some cached content", - @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}")) + @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached")) end def test_fragment_cache_instrumentation @@ -264,7 +281,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "<p>ERB</p>", - @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") end def test_xml_formatted_fragment_caching @@ -275,7 +292,7 @@ CACHED assert_equal expected_body, @response.body assert_equal " <p>Builder</p>\n", - @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") end def test_fragment_caching_with_variant @@ -286,7 +303,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "<p>PHONE</p>", - @store.read("views/test.host/functional_caching/formatted_fragment_cached_with_variant/#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}") + @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") end private @@ -412,7 +429,7 @@ class CollectionCacheTest < ActionController::TestCase def test_collection_fetches_cached_views get :index assert_equal 1, @controller.partial_rendered_times - assert_customer_cached "david/1", "david, 1" + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1") get :index assert_equal 1, @controller.partial_rendered_times @@ -444,14 +461,8 @@ class CollectionCacheTest < ActionController::TestCase def test_caching_with_callable_cache_key get :index_with_callable_cache_key - assert_customer_cached "cached_david", "david, 1" + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david") end - - private - def assert_customer_cached(key, content) - assert_match content, - ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937") - end end class FragmentCacheKeyTestController < CachingController @@ -470,11 +481,21 @@ class FragmentCacheKeyTest < ActionController::TestCase @controller.cache_store = @store end - def test_fragment_cache_key + def test_combined_fragment_cache_key @controller.account_id = "123" - assert_equal "views/v1/123/what a key", @controller.fragment_cache_key("what a key") + assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key") @controller.account_id = nil - assert_equal "views/v1//what a key", @controller.fragment_cache_key("what a key") + assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + end + + def test_combined_fragment_cache_key_with_envs + ENV["RAILS_APP_VERSION"] = "55" + assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + + ENV["RAILS_CACHE_ID"] = "66" + assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + ensure + ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index e0987070a3..5f1463cfa8 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" class ActionController::Base class << self - %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action skip_action_callback).each do |pending| + %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action).each do |pending| define_method(pending) do |*args| $stderr.puts "#{pending} unimplemented: #{args.inspect}" end unless method_defined?(pending) @@ -55,7 +55,7 @@ class FilterTest < ActionController::TestCase end end - protected + private (1..3).each do |i| define_method "try_#{i}" do instance_variable_set :@try, i @@ -296,7 +296,7 @@ class FilterTest < ActionController::TestCase render inline: "ran action" end - protected + private def find_user @ran_filter ||= [] @ran_filter << "find_user" @@ -428,7 +428,7 @@ class FilterTest < ActionController::TestCase render plain: "bar" end - protected + private def first @first = true end @@ -506,20 +506,20 @@ class FilterTest < ActionController::TestCase private def filter_one - @filters ||= [] - @filters << "filter_one" + @filters ||= [] + @filters << "filter_one" end def action_two - @filters << "action_two" + @filters << "action_two" end def non_yielding_action - @filters << "it didn't yield" + @filters << "it didn't yield" end def action_three - @filters << "action_three" + @filters << "action_three" end end @@ -584,7 +584,7 @@ class FilterTest < ActionController::TestCase assert @controller.instance_variable_get(:@was_audited) end - def test_running_anomolous_yet_valid_condition_actions + def test_running_anomalous_yet_valid_condition_actions test_process(AnomolousYetValidConditionController) assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) assert @controller.instance_variable_get(:@ran_class_action) @@ -704,7 +704,7 @@ class FilterTest < ActionController::TestCase def test_prepending_and_appending_around_action test_process(MixedFilterController) - assert_equal " before aroundfilter before procfilter before appended aroundfilter " + + assert_equal " before aroundfilter before procfilter before appended aroundfilter " \ " after appended aroundfilter after procfilter after aroundfilter ", MixedFilterController.execution_log end @@ -911,7 +911,7 @@ class ControllerWithFilterInstance < PostsController end class ControllerWithProcFilter < PostsController - around_action(only: :no_raise) do |c,b| + around_action(only: :no_raise) do |c, b| c.instance_variable_set(:"@before", true) b.call c.instance_variable_set(:"@after", true) @@ -956,46 +956,39 @@ class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters skip_after_action :after end -class SkipFilterUsingSkipActionCallback < ControllerWithAllTypesOfFilters - ActiveSupport::Deprecation.silence do - skip_action_callback :around_again - skip_action_callback :after - end -end - class YieldingAroundFiltersTest < ActionController::TestCase include PostsController::AroundExceptions def test_base controller = PostsController - assert_nothing_raised { test_process(controller,"no_raise") } - assert_nothing_raised { test_process(controller,"raises_before") } - assert_nothing_raised { test_process(controller,"raises_after") } - assert_nothing_raised { test_process(controller,"no_action") } + assert_nothing_raised { test_process(controller, "no_raise") } + assert_nothing_raised { test_process(controller, "raises_before") } + assert_nothing_raised { test_process(controller, "raises_after") } + assert_nothing_raised { test_process(controller, "no_action") } end def test_with_symbol controller = ControllerWithSymbolAsFilter - assert_nothing_raised { test_process(controller,"no_raise") } - assert_raise(Before) { test_process(controller,"raises_before") } - assert_raise(After) { test_process(controller,"raises_after") } - assert_nothing_raised { test_process(controller,"no_raise") } + assert_nothing_raised { test_process(controller, "no_raise") } + assert_raise(Before) { test_process(controller, "raises_before") } + assert_raise(After) { test_process(controller, "raises_after") } + assert_nothing_raised { test_process(controller, "no_raise") } end def test_with_class controller = ControllerWithFilterClass - assert_nothing_raised { test_process(controller,"no_raise") } - assert_raise(After) { test_process(controller,"raises_after") } + assert_nothing_raised { test_process(controller, "no_raise") } + assert_raise(After) { test_process(controller, "raises_after") } end def test_with_instance controller = ControllerWithFilterInstance - assert_nothing_raised { test_process(controller,"no_raise") } - assert_raise(After) { test_process(controller,"raises_after") } + assert_nothing_raised { test_process(controller, "no_raise") } + assert_raise(After) { test_process(controller, "raises_after") } end def test_with_proc - test_process(ControllerWithProcFilter,"no_raise") + test_process(ControllerWithProcFilter, "no_raise") assert @controller.instance_variable_get(:@before) assert @controller.instance_variable_get(:@after) end @@ -1004,25 +997,25 @@ class YieldingAroundFiltersTest < ActionController::TestCase controller = ControllerWithNestedFilters assert_nothing_raised do begin - test_process(controller,"raises_both") + test_process(controller, "raises_both") rescue Before, After end end assert_raise Before do begin - test_process(controller,"raises_both") + test_process(controller, "raises_both") rescue After end end end def test_action_order_with_all_action_types - test_process(ControllerWithAllTypesOfFilters,"no_raise") + test_process(ControllerWithAllTypesOfFilters, "no_raise") assert_equal "before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ") end def test_action_order_with_skip_action_method - test_process(ControllerWithTwoLessFilters,"no_raise") + test_process(ControllerWithTwoLessFilters, "no_raise") assert_equal "before around (before yield) around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ") end @@ -1047,28 +1040,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase assert_equal 3, controller.instance_variable_get(:@try) end - def test_skipping_with_skip_action_callback - test_process(SkipFilterUsingSkipActionCallback,"no_raise") - assert_equal "before around (before yield) around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ") - end - - def test_deprecated_skip_action_callback - assert_deprecated do - Class.new(PostsController) do - skip_action_callback :clean_up - end - end - end - - def test_deprecated_skip_filter - assert_deprecated do - Class.new(PostsController) do - skip_filter :clean_up - end - end - end - - protected + private def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller process(action) diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index 32f0db71f5..45b598a594 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -63,10 +63,10 @@ module ActionDispatch assert_equal({ "flashes" => { "foo" => "bar" }, "discard" => [] }, @hash.to_session_value) @hash.discard("foo") - assert_equal(nil, @hash.to_session_value) + assert_nil(@hash.to_session_value) @hash.sweep - assert_equal(nil, @hash.to_session_value) + assert_nil(@hash.to_session_value) end def test_from_session_value @@ -75,7 +75,7 @@ module ActionDispatch session = Marshal.load(Base64.decode64(rails_3_2_cookie)) hash = Flash::FlashHash.from_session_value(session["flash"]) assert_equal({ "greeting" => "Hello" }, hash.to_hash) - assert_equal(nil, hash.to_session_value) + assert_nil(hash.to_session_value) end def test_from_session_value_on_json_serializer @@ -84,7 +84,7 @@ module ActionDispatch hash = Flash::FlashHash.from_session_value(session["flash"]) assert_equal({ "greeting" => "Hello" }, hash.to_hash) - assert_equal(nil, hash.to_session_value) + assert_nil(hash.to_session_value) assert_equal "Hello", hash[:greeting] assert_equal "Hello", hash["greeting"] end @@ -102,8 +102,8 @@ module ActionDispatch @hash["foo"] = "bar" things = [] - @hash.each do |k,v| - things << [k,v] + @hash.each do |k, v| + things << [k, v] end assert_equal([%w{ hello world }, %w{ foo bar }].sort, things.sort) diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 4c6a772062..03dbd63614 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -ActionController::Base.helpers_path = File.expand_path("../../fixtures/helpers", __FILE__) +ActionController::Base.helpers_path = File.expand_path("../fixtures/helpers", __dir__) module Fun class GamesController < ActionController::Base @@ -48,7 +48,7 @@ end class HelpersPathsController < ActionController::Base paths = ["helpers2_pack", "helpers1_pack"].map do |path| - File.join(File.expand_path("../../fixtures", __FILE__), path) + File.join(File.expand_path("../fixtures", __dir__), path) end $:.unshift(*paths) @@ -61,7 +61,7 @@ class HelpersPathsController < ActionController::Base end class HelpersTypoController < ActionController::Base - path = File.expand_path("../../fixtures/helpers_typo", __FILE__) + path = File.expand_path("../fixtures/helpers_typo", __dir__) $:.unshift(path) self.helpers_path = path end @@ -178,7 +178,7 @@ class HelperTest < ActiveSupport::TestCase end def test_all_helpers_with_alternate_helper_dir - @controller_class.helpers_path = File.expand_path("../../fixtures/alternate_helpers", __FILE__) + @controller_class.helpers_path = File.expand_path("../fixtures/alternate_helpers", __dir__) # Reload helpers @controller_class._helpers = Module.new diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 343b7b643d..0b59e123d7 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -7,7 +7,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase before_action :authenticate_with_request, only: :display USERS = { "lifo" => "world", "pretty" => "please", - "dhh" => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":")) } + "dhh" => ::Digest::MD5::hexdigest(["dhh", "SuperSecret", "secret"].join(":")) } def index render plain: "Hello Secret" @@ -180,7 +180,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase test "authentication request with password stored as ha1 digest hash" do @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "dhh", - password: ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":")), + password: ::Digest::MD5::hexdigest(["dhh", "SuperSecret", "secret"].join(":")), password_is_ha1: true) get :display diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 3842136682..09d2793c9a 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -166,8 +166,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase test "token_and_options returns nil with no value after the equal sign" do actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first - expected = nil - assert_equal(expected, actual) + assert_nil actual end test "raw_params returns a tuple of two key value pair strings" do diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index d3bc77d3ef..72163ccd5e 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -31,95 +31,6 @@ class SessionTest < ActiveSupport::TestCase end end - def test_request_via_redirect_uses_given_method - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - assert_called_with @session, :process, [:put, path, params: args, headers: headers] do - @session.stub :redirect?, false do - assert_deprecated { @session.request_via_redirect(:put, path, params: args, headers: headers) } - end - end - end - - def test_deprecated_request_via_redirect_uses_given_method - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - assert_called_with @session, :process, [:put, path, params: args, headers: headers] do - @session.stub :redirect?, false do - assert_deprecated { @session.request_via_redirect(:put, path, args, headers) } - end - end - end - - def test_request_via_redirect_follows_redirects - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - value_series = [true, true, false] - assert_called @session, :follow_redirect!, times: 2 do - @session.stub :redirect?, -> { value_series.shift } do - assert_deprecated { @session.request_via_redirect(:get, path, params: args, headers: headers) } - end - end - end - - def test_request_via_redirect_returns_status - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - @session.stub :redirect?, false do - @session.stub :status, 200 do - assert_deprecated do - assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers) - end - end - end - end - - def test_deprecated_get_via_redirect - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - - assert_called_with @session, :request_via_redirect, [:get, path, args, headers] do - assert_deprecated do - @session.get_via_redirect(path, args, headers) - end - end - end - - def test_deprecated_post_via_redirect - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - - assert_called_with @session, :request_via_redirect, [:post, path, args, headers] do - assert_deprecated do - @session.post_via_redirect(path, args, headers) - end - end - end - - def test_deprecated_patch_via_redirect - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - - assert_called_with @session, :request_via_redirect, [:patch, path, args, headers] do - assert_deprecated do - @session.patch_via_redirect(path, args, headers) - end - end - end - - def test_deprecated_put_via_redirect - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - - assert_called_with @session, :request_via_redirect, [:put, path, args, headers] do - assert_deprecated do - @session.put_via_redirect(path, args, headers) - end - end - end - - def test_deprecated_delete_via_redirect - path = "/somepath"; args = { id: "1" }; headers = { "X-Test-Header" => "testvalue" } - - assert_called_with @session, :request_via_redirect, [:delete, path, args, headers] do - assert_deprecated do - @session.delete_via_redirect(path, args, headers) - end - end - end - def test_get path = "/index"; params = "blah"; headers = { location: "blah" } @@ -135,28 +46,9 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_get - path = "/index"; params = "blah"; headers = { location: "blah" } - - assert_called_with @session, :process, [:get, path, params: params, headers: headers] do - assert_deprecated { - @session.get(path, params, headers) - } - end - end - def test_post path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:post, path, params: params, headers: headers] do - assert_deprecated { - @session.post(path, params, headers) - } - end - end - - def test_deprecated_post - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:post, path, params: params, headers: headers] do @session.post(path, params: params, headers: headers) end end @@ -168,15 +60,6 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_patch - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do - assert_deprecated { - @session.patch(path, params, headers) - } - end - end - def test_put path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:put, path, params: params, headers: headers] do @@ -184,27 +67,9 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_put - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:put, path, params: params, headers: headers] do - assert_deprecated { - @session.put(path, params, headers) - } - end - end - def test_delete path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do - assert_deprecated { - @session.delete(path,params,headers) - } - end - end - - def test_deprecated_delete - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do @session.delete(path, params: params, headers: headers) end end @@ -216,15 +81,6 @@ class SessionTest < ActiveSupport::TestCase end end - def deprecated_test_head - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:head, path, params: params, headers: headers] do - assert_deprecated { - @session.head(path, params, headers) - } - end - end - def test_xml_http_request_get path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do @@ -232,22 +88,6 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_xml_http_request_get - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do - @session.get(path, params: params, headers: headers, xhr: true) - end - end - - def test_deprecated_args_xml_http_request_get - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do - assert_deprecated(/xml_http_request/) { - @session.xml_http_request(:get, path, params, headers) - } - end - end - def test_xml_http_request_post path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do @@ -255,20 +95,6 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_xml_http_request_post - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do - @session.post(path, params: params, headers: headers, xhr: true) - end - end - - def test_deprecated_args_xml_http_request_post - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do - assert_deprecated(/xml_http_request/) { @session.xml_http_request(:post,path,params,headers) } - end - end - def test_xml_http_request_patch path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do @@ -276,20 +102,6 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_xml_http_request_patch - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do - @session.patch(path, params: params, headers: headers, xhr: true) - end - end - - def test_deprecated_args_xml_http_request_patch - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do - assert_deprecated(/xml_http_request/) { @session.xml_http_request(:patch,path,params,headers) } - end - end - def test_xml_http_request_put path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do @@ -297,20 +109,6 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_xml_http_request_put - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do - @session.put(path, params: params, headers: headers, xhr: true) - end - end - - def test_deprecated_args_xml_http_request_put - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do - assert_deprecated(/xml_http_request/) { @session.xml_http_request(:put, path, params, headers) } - end - end - def test_xml_http_request_delete path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do @@ -318,40 +116,12 @@ class SessionTest < ActiveSupport::TestCase end end - def test_deprecated_xml_http_request_delete - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do - assert_deprecated { @session.xml_http_request(:delete, path, params: params, headers: headers) } - end - end - - def test_deprecated_args_xml_http_request_delete - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do - assert_deprecated(/xml_http_request/) { @session.xml_http_request(:delete, path, params, headers) } - end - end - def test_xml_http_request_head path = "/index"; params = "blah"; headers = { location: "blah" } assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do @session.head(path, params: params, headers: headers, xhr: true) end end - - def test_deprecated_xml_http_request_head - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do - assert_deprecated(/xml_http_request/) { @session.xml_http_request(:head, path, params: params, headers: headers) } - end - end - - def test_deprecated_args_xml_http_request_head - path = "/index"; params = "blah"; headers = { location: "blah" } - assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do - assert_deprecated { @session.xml_http_request(:head, path, params, headers) } - end - end end class IntegrationTestTest < ActiveSupport::TestCase @@ -375,7 +145,7 @@ class IntegrationTestTest < ActiveSupport::TestCase name.to_s == "foo" ? "pass" : super end end - @test.class.superclass.__send__(:include, mixin) + @test.class.superclass.include(mixin) begin assert_equal "pass", @test.foo ensure @@ -499,11 +269,11 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest test "response cookies are added to the cookie jar for the next request" do with_test_route_set do - self.cookies["cookie_1"] = "sugar" - self.cookies["cookie_2"] = "oatmeal" + cookies["cookie_1"] = "sugar" + cookies["cookie_2"] = "oatmeal" get "/cookie_monster" assert_equal "cookie_1=; path=/\ncookie_3=chocolate; path=/", headers["Set-Cookie"] - assert_equal({ "cookie_1"=>"", "cookie_2"=>"oatmeal", "cookie_3"=>"chocolate" }, cookies.to_hash) + assert_equal({ "cookie_1" => "", "cookie_2" => "oatmeal", "cookie_3" => "chocolate" }, cookies.to_hash) end end @@ -513,14 +283,14 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_response :success assert_equal "foo=bar; path=/", headers["Set-Cookie"] - assert_equal({ "foo"=>"bar" }, cookies.to_hash) + assert_equal({ "foo" => "bar" }, cookies.to_hash) get "/get_cookie" assert_response :success assert_equal "bar", body - assert_equal nil, headers["Set-Cookie"] - assert_equal({ "foo"=>"bar" }, cookies.to_hash) + assert_nil headers["Set-Cookie"] + assert_equal({ "foo" => "bar" }, cookies.to_hash) end end @@ -532,14 +302,14 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_response :success assert_equal "foo=bar; path=/", headers["Set-Cookie"] - assert_equal({ "foo"=>"bar" }, cookies.to_hash) + assert_equal({ "foo" => "bar" }, cookies.to_hash) get "/get_cookie" assert_response :success assert_equal "bar", body - assert_equal nil, headers["Set-Cookie"] - assert_equal({ "foo"=>"bar" }, cookies.to_hash) + assert_nil headers["Set-Cookie"] + assert_equal({ "foo" => "bar" }, cookies.to_hash) end end @@ -577,18 +347,6 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end - def test_deprecated_xml_http_request_get - with_test_route_set do - assert_deprecated { xhr :get, "/get" } - assert_equal 200, status - assert_equal "OK", status_message - assert_response 200 - assert_response :success - assert_response :ok - assert_equal "JS OK", response.body - end - end - def test_request_with_bad_format with_test_route_set do get "/get.php", xhr: true @@ -598,6 +356,14 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + test "creation of multiple integration sessions" do + integration_session # initialize first session + a = open_session + b = open_session + + refute_same(a.integration_session, b.integration_session) + end + def test_get_with_query_string with_test_route_set do get "/get_with_params?foo=bar" @@ -728,7 +494,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_includes @response.headers, "c" end - def test_accept_not_overriden_when_xhr_true + def test_accept_not_overridden_when_xhr_true with_test_route_set do get "/get", headers: { "Accept" => "application/json" }, xhr: true assert_equal "application/json", request.accept @@ -1158,12 +924,16 @@ class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest def test_cookies_set_in_setup_are_persisted_through_the_session get "/foo" - assert_equal({ "user_name"=>"david" }, cookies.to_hash) + assert_equal({ "user_name" => "david" }, cookies.to_hash) end end class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest class FooController < ActionController::Base + def foos + render plain: "ok" + end + def foos_json render json: params.permit(:foo) end @@ -1192,13 +962,30 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest def test_encoding_as_json post_to_foos as: :json do assert_response :success - assert_match "foos_json.json", request.path assert_equal "application/json", request.content_type + assert_equal "application/json", request.accepts.first.to_s + assert_equal :json, request.format.ref assert_equal({ "foo" => "fighters" }, request.request_parameters) assert_equal({ "foo" => "fighters" }, response.parsed_body) end end + def test_doesnt_mangle_request_path + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + post ":action" => FooController + end + end + + post "/foos" + assert_equal "/foos", request.path + + post "/foos_json", as: :json + assert_equal "/foos_json", request.path + end + end + def test_encoding_as_without_mime_registration assert_raise ArgumentError do ActionDispatch::IntegrationTest.register_encoder :wibble @@ -1213,8 +1000,10 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest post_to_foos as: :wibble do assert_response :success - assert_match "foos_wibble.wibble", request.path + assert_equal "/foos_wibble", request.path assert_equal "text/wibble", request.content_type + assert_equal "text/wibble", request.accepts.first.to_s + assert_equal :wibble, request.format.ref assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed. assert_equal "ok", response.parsed_body end @@ -1302,7 +1091,7 @@ class IntegrationFileUploadTest < ActionDispatch::IntegrationTest end def self.fixture_path - File.dirname(__FILE__) + "/../fixtures/multipart" + File.expand_path("../fixtures/multipart", __dir__) end routes.draw do @@ -1312,8 +1101,8 @@ class IntegrationFileUploadTest < ActionDispatch::IntegrationTest def test_fixture_file_upload post "/test_file_upload", params: { - file: fixture_file_upload("/mona_lisa.jpg", "image/jpg") + file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpg") } - assert_equal "159528", @response.body + assert_equal "45142", @response.body end end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 1361e95081..bfb47b90d5 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -1,4 +1,5 @@ require "abstract_unit" +require "timeout" require "concurrent/atomic/count_down_latch" Thread.abort_on_exception = true @@ -151,13 +152,15 @@ module ActionController end def write_sleep_autoload - path = File.join(File.dirname(__FILE__), "../fixtures") + path = File.expand_path("../fixtures", __dir__) ActiveSupport::Dependencies.autoload_paths << path response.headers["Content-Type"] = "text/event-stream" response.stream.write "before load" sleep 0.01 - ::LoadMe + silence_warning do + ::LoadMe + end response.stream.close latch.count_down diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb new file mode 100644 index 0000000000..e16452ed6f --- /dev/null +++ b/actionpack/test/controller/metal_test.rb @@ -0,0 +1,30 @@ +require "abstract_unit" + +class MetalControllerInstanceTests < ActiveSupport::TestCase + class SimpleController < ActionController::Metal + def hello + self.response_body = "hello" + end + end + + def test_response_has_default_headers + original_default_headers = ActionDispatch::Response.default_headers + + ActionDispatch::Response.default_headers = { + "X-Frame-Options" => "DENY", + "X-Content-Type-Options" => "nosniff", + "X-XSS-Protection" => "1;" + } + + response_headers = SimpleController.action("hello").call( + "REQUEST_METHOD" => "GET", + "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") + ensure + ActionDispatch::Response.default_headers = original_default_headers + end +end diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb index a2834c9f39..d1c4dbfef7 100644 --- a/actionpack/test/controller/mime/accept_format_test.rb +++ b/actionpack/test/controller/mime/accept_format_test.rb @@ -29,7 +29,7 @@ class StarStarMimeControllerTest < ActionController::TestCase end class AbstractPostController < ActionController::Base - self.view_paths = File.dirname(__FILE__) + "/../../fixtures/post_test/" + self.view_paths = File.expand_path("../../fixtures/post_test", __dir__) end # For testing layouts which are set automatically @@ -40,7 +40,7 @@ class PostController < AbstractPostController respond_to(:html, :iphone, :js) end -protected +private def with_iphone request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone" diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index c5f8165d04..61bd5c80c4 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -273,7 +273,7 @@ class RespondToController < ActionController::Base end end - protected + private def set_layout case action_name when "all_types_with_layout", "iphone_with_html_response_type" @@ -519,7 +519,7 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "Whatever you ask for, I got it", @response.body end - def test_handle_any_any_unkown_format + def test_handle_any_any_unknown_format get :handle_any_any, format: "php" assert_equal "Whatever you ask for, I got it", @response.body end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 9956f0b107..054757fab3 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -52,7 +52,7 @@ module BareMetalTest controller.set_request!(ActionDispatch::Request.empty) controller.set_response!(BareController.make_response!(controller.request)) controller.index - assert_equal nil, controller.response_body + assert_nil controller.response_body end end diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb index 3765d406fc..b891df4c0f 100644 --- a/actionpack/test/controller/new_base/base_test.rb +++ b/actionpack/test/controller/new_base/base_test.rb @@ -25,7 +25,7 @@ module Dispatching render body: "actions: #{action_methods.to_a.sort.join(', ')}" end - protected + private def authenticate end end diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb index b64468a94e..25b73ac78c 100644 --- a/actionpack/test/controller/new_base/render_context_test.rb +++ b/actionpack/test/controller/new_base/render_context_test.rb @@ -26,16 +26,14 @@ module RenderContext render action: "hello_world", layout: "basic" end - protected - - # 3) Set view_context to self - def view_context - self - end + protected def __controller_method__ + "controller context!" + end - def __controller_method__ - "controller context!" - end + # 3) Set view_context to self + private def view_context + self + end end class RenderContextTest < Rack::TestCase diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb index 6d651e0104..4491dd96ed 100644 --- a/actionpack/test/controller/new_base/render_file_test.rb +++ b/actionpack/test/controller/new_base/render_file_test.rb @@ -2,15 +2,15 @@ require "abstract_unit" module RenderFile class BasicController < ActionController::Base - self.view_paths = File.dirname(__FILE__) + self.view_paths = __dir__ def index - render file: File.join(File.dirname(__FILE__), *%w[.. .. fixtures test hello_world]) + render file: File.expand_path("../../fixtures/test/hello_world", __dir__) end def with_instance_variables @secret = "in the sauce" - render file: File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_ivar") + render file: File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) end def relative_path @@ -25,11 +25,11 @@ module RenderFile def pathname @secret = "in the sauce" - render file: Pathname.new(File.dirname(__FILE__)).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar]) + render file: Pathname.new(__dir__).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar]) end def with_locals - path = File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_locals") + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) render file: path, locals: { secret: "in the sauce" } end end diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb index 796283466a..c5fc8e15e1 100644 --- a/actionpack/test/controller/new_base/render_implicit_action_test.rb +++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb @@ -6,7 +6,7 @@ module RenderImplicitAction "render_implicit_action/simple/hello_world.html.erb" => "Hello world!", "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!", "render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented" - ), ActionView::FileSystemResolver.new(File.expand_path("../../../controller", __FILE__))] + ), ActionView::FileSystemResolver.new(File.expand_path("../../controller", __dir__))] def hello_world() end end diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb index 5cd8f82323..1177b8b03e 100644 --- a/actionpack/test/controller/new_base/render_streaming_test.rb +++ b/actionpack/test/controller/new_base/render_streaming_test.rb @@ -101,12 +101,12 @@ module RenderStreaming assert_body "Hello world, I'm here!" assert_status 200 assert_equal "22", headers["Content-Length"] - assert_equal nil, headers["Transfer-Encoding"] + assert_nil headers["Transfer-Encoding"] end - def assert_streaming!(cache="no-cache") + def assert_streaming!(cache = "no-cache") assert_status 200 - assert_equal nil, headers["Content-Length"] + assert_nil headers["Content-Length"] assert_equal "chunked", headers["Transfer-Encoding"] assert_equal cache, headers["Cache-Control"] end diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb deleted file mode 100644 index 6f55c497b4..0000000000 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ /dev/null @@ -1,188 +0,0 @@ -require "abstract_unit" - -module RenderText - class MinimalController < ActionController::Metal - include AbstractController::Rendering - include ActionController::Rendering - - def index - render text: "Hello World!" - end - end - - class SimpleController < ActionController::Base - self.view_paths = [ActionView::FixtureResolver.new] - - def index - render text: "hello david" - end - end - - class WithLayoutController < ::ApplicationController - self.view_paths = [ActionView::FixtureResolver.new( - "layouts/application.html.erb" => "<%= yield %>, I'm here!", - "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.", - "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>" - )] - - def index - render text: "hello david" - end - - def custom_code - render text: "hello world", status: 404 - end - - def with_custom_code_as_string - render text: "hello world", status: "404 Not Found" - end - - def with_nil - render text: nil - end - - def with_nil_and_status - render text: nil, status: 403 - end - - def with_false - render text: false - end - - def with_layout_true - render text: "hello world", layout: true - end - - def with_layout_false - render text: "hello world", layout: false - end - - def with_layout_nil - render text: "hello world", layout: nil - end - - def with_custom_layout - render text: "hello world", layout: "greetings" - end - - def with_ivar_in_layout - @ivar = "hello world" - render text: "hello world", layout: "ivar" - end - end - - class RenderTextTest < Rack::TestCase - test "rendering text from a minimal controller" do - ActiveSupport::Deprecation.silence do - get "/render_text/minimal/index" - end - - assert_body "Hello World!" - assert_status 200 - end - - test "rendering text from an action with default options renders the text with the layout" do - with_routing do |set| - set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } } - - ActiveSupport::Deprecation.silence do - get "/render_text/simple" - end - - assert_body "hello david" - assert_status 200 - end - end - - test "rendering text from an action with default options renders the text without the layout" do - with_routing do |set| - set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } } - - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout" - end - - assert_body "hello david" - assert_status 200 - end - end - - test "rendering text, while also providing a custom status code" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/custom_code" - end - - assert_body "hello world" - assert_status 404 - end - - test "rendering text with nil returns an empty body" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_nil" - end - - assert_body "" - assert_status 200 - end - - test "Rendering text with nil and custom status code returns an empty body and the status" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_nil_and_status" - end - - assert_body "" - assert_status 403 - end - - test "rendering text with false returns the string 'false'" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_false" - end - - assert_body "false" - assert_status 200 - end - - test "rendering text with layout: true" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_layout_true" - end - - assert_body "hello world, I'm here!" - assert_status 200 - end - - test "rendering text with layout: 'greetings'" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_custom_layout" - end - - assert_body "hello world, I wish thee well." - assert_status 200 - end - - test "rendering text with layout: false" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_layout_false" - end - - assert_body "hello world" - assert_status 200 - end - - test "rendering text with layout: nil" do - ActiveSupport::Deprecation.silence do - get "/render_text/with_layout/with_layout_nil" - end - - assert_body "hello world" - assert_status 200 - end - - test "rendering text displays deprecation warning" do - assert_deprecated do - get "/render_text/with_layout/with_layout_nil" - end - end - end -end diff --git a/actionpack/test/controller/parameter_encoding_test.rb b/actionpack/test/controller/parameter_encoding_test.rb index 7840b4f5c4..234d0bddd1 100644 --- a/actionpack/test/controller/parameter_encoding_test.rb +++ b/actionpack/test/controller/parameter_encoding_test.rb @@ -1,9 +1,8 @@ require "abstract_unit" class ParameterEncodingController < ActionController::Base - parameter_encoding :test_bar, :bar, Encoding::ASCII_8BIT - parameter_encoding :test_baz, :baz, Encoding::ISO_8859_1 - parameter_encoding :test_baz_to_ascii, :baz, Encoding::ASCII_8BIT + skip_parameter_encoding :test_bar + skip_parameter_encoding :test_all_values_encoding def test_foo render body: params[:foo].encoding @@ -13,16 +12,8 @@ class ParameterEncodingController < ActionController::Base render body: params[:bar].encoding end - def test_baz - render body: params[:baz].encoding - end - - def test_no_change_to_baz - render body: params[:baz].encoding - end - - def test_baz_to_ascii - render body: params[:baz].encoding + def test_all_values_encoding + render body: ::JSON.dump(params.values.map(&:encoding).map(&:name)) end end @@ -36,32 +27,18 @@ class ParameterEncodingTest < ActionController::TestCase assert_equal "UTF-8", @response.body end - test "properly transcodes ASCII_8BIT parameters into declared encodings" do + test "properly encodes ASCII_8BIT parameters into binary" do post :test_bar, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } assert_response :success assert_equal "ASCII-8BIT", @response.body end - test "properly transcodes ISO_8859_1 parameters into declared encodings" do - post :test_baz, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } - - assert_response :success - assert_equal "ISO-8859-1", @response.body - end - - test "does not transcode parameters when not specified" do - post :test_no_change_to_baz, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } + test "properly encodes all ASCII_8BIT parameters into binary" do + post :test_all_values_encoding, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } assert_response :success - assert_equal "UTF-8", @response.body - end - - test "respects different encoding declarations for a param per action" do - post :test_baz_to_ascii, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } - - assert_response :success - assert_equal "ASCII-8BIT", @response.body + assert_equal ["ASCII-8BIT"], JSON.parse(@response.body).uniq end test "does not raise an error when passed a param declared as ASCII-8BIT that contains invalid bytes" do diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 8a522b2df8..7725c25e22 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -53,6 +53,15 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" } end + test "empty? returns true when params contains no key/value pairs" do + params = ActionController::Parameters.new + assert params.empty? + end + + test "empty? returns false when any params are present" do + refute @params.empty? + end + test "except retains permitted status" do @params.permit! assert @params.except(:person).permitted? @@ -75,6 +84,45 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params[:person].fetch(:name).permitted? end + test "has_key? returns true if the given key is present in the params" do + assert @params.has_key?(:person) + end + + test "has_key? returns false if the given key is not present in the params" do + refute @params.has_key?(:address) + end + + test "has_value? returns true if the given value is present in the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert params.has_value?("Chicago") + end + + 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") + end + + test "include? returns true if the given key is present in the params" do + assert @params.include?(:person) + end + + test "include? returns false if the given key is not present in the params" do + refute @params.include?(:address) + end + + test "key? returns true if the given key is present in the params" do + assert @params.key?(:person) + end + + test "key? returns false if the given key is not present in the params" do + refute @params.key?(:address) + end + + test "keys returns an array of the keys of the params" do + assert_equal ["person"], @params.keys + assert_equal ["age", "name", "addresses"], @params[:person].keys + end + test "reject retains permitted status" do assert_not @params.reject { |k| k == "person" }.permitted? end @@ -120,6 +168,21 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params.transform_values { |v| v }.permitted? end + test "value? returns true if the given value is present in the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert params.value?("Chicago") + end + + 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") + end + + test "values returns an array of the values of the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert_equal ["Chicago", "Illinois"], params.values + end + test "values_at retains permitted status" do @params.permit! assert @params.values_at(:person).first.permitted? @@ -131,14 +194,6 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params[:person].values_at(:name).first.permitted? end - test "equality with a hash is deprecated" do - hash1 = { foo: :bar } - params1 = ActionController::Parameters.new(hash1) - assert_deprecated("will be removed in Rails 5.1") do - assert(params1 == hash1) - end - end - test "is equal to Parameters instance with same params" do params1 = ActionController::Parameters.new(a: 1, b: 2) params2 = ActionController::Parameters.new(a: 1, b: 2) diff --git a/actionpack/test/controller/parameters/dup_test.rb b/actionpack/test/controller/parameters/dup_test.rb index d88891ca30..fb707a1354 100644 --- a/actionpack/test/controller/parameters/dup_test.rb +++ b/actionpack/test/controller/parameters/dup_test.rb @@ -1,5 +1,6 @@ require "abstract_unit" require "action_controller/metal/strong_parameters" +require "active_support/core_ext/object/deep_dup" class ParametersDupTest < ActiveSupport::TestCase setup do @@ -40,4 +41,25 @@ class ParametersDupTest < ActiveSupport::TestCase dupped_params.permit! assert_not_equal @params, dupped_params end + + test "deep_dup content" do + dupped_params = @params.deep_dup + dupped_params[:person][:age] = "45" + dupped_params[:person][:addresses].clear + + assert_not_equal @params[:person][:age], dupped_params[:person][:age] + assert_not_equal @params[:person][:addresses], dupped_params[:person][:addresses] + end + + test "deep_dup @permitted" do + dupped_params = @params.deep_dup + dupped_params.permit! + + assert_not @params.permitted? + end + + test "deep_dup @permitted is being copied" do + @params.permit! + assert @params.deep_dup.permitted? + end end diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb index e060e5180f..2c36f488c6 100644 --- a/actionpack/test/controller/parameters/mutators_test.rb +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -25,6 +25,27 @@ class ParametersMutatorsTest < ActiveSupport::TestCase assert_not @params.delete(:person).permitted? end + test "delete returns the value when the key is present" do + assert_equal "32", @params[:person].delete(:age) + end + + test "delete removes the entry when the key present" do + @params[:person].delete(:age) + assert_not @params[:person].key?(:age) + end + + test "delete returns nil when the key is not present" do + assert_equal nil, @params[:person].delete(:first_name) + end + + test "delete returns the value of the given block when the key is not present" do + assert_equal "David", @params[:person].delete(:first_name) { "David" } + end + + test "delete yields the key to the given block when the key is not present" do + assert_equal "first_name: David", @params[:person].delete(:first_name) { |k| "#{k}: David" } + end + test "delete_if retains permitted status" do @params.permit! assert @params.delete_if { |k| k == "person" }.permitted? @@ -45,11 +66,11 @@ class ParametersMutatorsTest < ActiveSupport::TestCase test "keep_if retains permitted status" do @params.permit! - assert @params.keep_if { |k,v| k == "person" }.permitted? + assert @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 @params.keep_if { |k, v| k == "person" }.permitted? end test "reject! retains permitted status" do diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb index 5f86901e30..00e591d5a7 100644 --- a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb @@ -140,6 +140,11 @@ class NestedParametersPermitTest < ActiveSupport::TestCase assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["0"][:name] assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["1"][:name] + assert_equal( + { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare" }, "1" => { "name" => "Unattributed Assistant" }, "2" => {} } } }, + permitted.to_h + ) + assert_filtered_out permitted[:book][:authors_attributes]["0"], :age_of_death end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 728d8e1279..ae2b45c9f0 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -28,7 +28,7 @@ class ParametersPermitTest < ActiveSupport::TestCase end def walk_permitted(params) - params.each do |k,v| + params.each do |k, v| case v when ActionController::Parameters walk_permitted v @@ -39,13 +39,13 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "iteration should not impact permit" do - hash = { "foo"=>{ "bar"=>{ "0"=>{ "baz"=>"hello", "zot"=>"1" } } } } + hash = { "foo" => { "bar" => { "0" => { "baz" => "hello", "zot" => "1" } } } } params = ActionController::Parameters.new(hash) walk_permitted params sanitized = params[:foo].permit(bar: [:baz]) - assert_equal({ "0"=>{ "baz"=>"hello" } }, sanitized[:bar].to_unsafe_h) + assert_equal({ "0" => { "baz" => "hello" } }, sanitized[:bar].to_unsafe_h) end test "if nothing is permitted, the hash becomes empty" do @@ -66,12 +66,20 @@ class ParametersPermitTest < ActiveSupport::TestCase values.each do |value| params = ActionController::Parameters.new(id: value) permitted = params.permit(:id) - assert_equal value, permitted[:id] + if value.nil? + assert_nil permitted[:id] + else + assert_equal value, permitted[:id] + end @struct_fields.each do |sf| params = ActionController::Parameters.new(sf => value) permitted = params.permit(:sf) - assert_equal value, permitted[sf] + if value.nil? + assert_nil permitted[sf] + else + assert_equal value, permitted[sf] + end end end end @@ -141,7 +149,7 @@ class ParametersPermitTest < ActiveSupport::TestCase permitted = params.permit(:a, c: [], b: []) assert_equal 1, permitted[:a] assert_equal [1, 2, 3], permitted[:b] - assert_equal nil, permitted[:c] + assert_nil permitted[:c] end test "key to empty array: arrays of permitted scalars pass" do @@ -168,6 +176,39 @@ class ParametersPermitTest < ActiveSupport::TestCase end end + test "key to empty hash: arbitrary hashes are permitted" do + params = ActionController::Parameters.new( + username: "fxn", + preferences: { + scheme: "Marazul", + font: { + name: "Source Code Pro", + size: 12 + }, + tabstops: [4, 8, 12, 16], + suspicious: [true, Object.new, false, /yo!/], + dubious: [{ a: :a, b: /wtf!/ }, { c: :c }], + injected: Object.new + }, + hacked: 1 # not a hash + ) + + permitted = params.permit(:username, preferences: {}, hacked: {}) + + assert_equal "fxn", permitted[:username] + assert_equal "Marazul", permitted[:preferences][:scheme] + assert_equal "Source Code Pro", permitted[:preferences][:font][:name] + assert_equal 12, permitted[:preferences][:font][:size] + assert_equal [4, 8, 12, 16], permitted[:preferences][:tabstops] + assert_equal [true, false], permitted[:preferences][:suspicious] + assert_equal :a, permitted[:preferences][:dubious][0][:a] + assert_equal :c, permitted[:preferences][:dubious][1][:c] + + assert_filtered_out permitted[:preferences][:dubious][0], :b + assert_filtered_out permitted[:preferences], :injected + assert_filtered_out permitted, :hacked + end + test "fetch raises ParameterMissing exception" do e = assert_raises(ActionController::ParameterMissing) do @params.fetch :foo @@ -178,7 +219,7 @@ class ParametersPermitTest < ActiveSupport::TestCase test "fetch with a default value of a hash does not mutate the object" do params = ActionController::Parameters.new({}) params.fetch :foo, {} - assert_equal nil, params[:foo] + assert_nil params[:foo] end test "hashes in array values get wrapped" do @@ -216,8 +257,8 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do - assert_equal nil, @params.fetch(:foo, nil) - assert_equal nil, @params.fetch(:foo) { nil } + assert_nil @params.fetch(:foo, nil) + assert_nil @params.fetch(:foo) { nil } end test "KeyError in fetch block should not be covered up" do @@ -244,6 +285,64 @@ class ParametersPermitTest < ActiveSupport::TestCase assert merged_params[:id] end + test "not permitted is sticky beyond merge!" do + assert_not @params.merge!(a: "b").permitted? + end + + test "permitted is sticky beyond merge!" do + @params.permit! + assert @params.merge!(a: "b").permitted? + end + + test "merge! with parameters" do + other_params = ActionController::Parameters.new(id: "1234").permit! + @params.merge!(other_params) + + assert_equal "1234", @params[:id] + assert_equal "32", @params[:person][:age] + end + + test "#reverse_merge with parameters" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + merged_params = @params.reverse_merge(default_params) + + assert_equal "1234", merged_params[:id] + refute_predicate merged_params[:person], :empty? + end + + test "#with_defaults is an alias of reverse_merge" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + merged_params = @params.with_defaults(default_params) + + assert_equal "1234", merged_params[:id] + refute_predicate merged_params[:person], :empty? + end + + test "not permitted is sticky beyond reverse_merge" do + refute_predicate @params.reverse_merge(a: "b"), :permitted? + end + + test "permitted is sticky beyond reverse_merge" do + @params.permit! + assert_predicate @params.reverse_merge(a: "b"), :permitted? + end + + test "#reverse_merge! with parameters" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + @params.reverse_merge!(default_params) + + assert_equal "1234", @params[:id] + refute_predicate @params[:person], :empty? + end + + test "#with_defaults! is an alias of reverse_merge!" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + @params.with_defaults!(default_params) + + assert_equal "1234", @params[:id] + refute_predicate @params[:person], :empty? + end + test "modifying the parameters" do @params[:person][:hometown] = "Chicago" @params[:person][:family] = { brother: "Jonas" } @@ -278,17 +377,17 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "32", @params[:person].permit([ :age ])[:age] end - test "to_h returns empty hash on unpermitted params" do - assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters - assert @params.to_h.empty? + test "to_h raises UnfilteredParameters on unfiltered params" do + assert_raises(ActionController::UnfilteredParameters) do + @params.to_h + end end test "to_h returns converted hash on permitted params" do @params.permit! - assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_h + assert_not_kind_of ActionController::Parameters, @params.to_h end test "to_h returns converted hash when .permit_all_parameters is set" do @@ -296,39 +395,71 @@ class ParametersPermitTest < ActiveSupport::TestCase ActionController::Parameters.permit_all_parameters = true params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") - assert params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters + assert_instance_of ActiveSupport::HashWithIndifferentAccess, params.to_h + assert_not_kind_of ActionController::Parameters, params.to_h assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h) ensure ActionController::Parameters.permit_all_parameters = false end end - test "to_h returns always permitted parameter on unpermitted params" do - params = ActionController::Parameters.new( - controller: "users", - action: "create", - user: { - name: "Sengoku Nadeko" - } - ) + test "to_hash raises UnfilteredParameters on unfiltered params" do + assert_raises(ActionController::UnfilteredParameters) do + @params.to_hash + end + end + + test "to_hash returns converted hash on permitted params" do + @params.permit! + + assert_instance_of Hash, @params.to_hash + assert_not_kind_of ActionController::Parameters, @params.to_hash + end - assert_equal({ "controller" => "users", "action" => "create" }, params.to_h) + test "parameters can be implicit converted to Hash" do + params = ActionController::Parameters.new + params.permit! + + assert_equal({ a: 1 }, { a: 1 }.merge!(params)) + end + + test "to_hash returns converted hash when .permit_all_parameters is set" do + begin + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") + + assert_instance_of Hash, params.to_hash + assert_not_kind_of ActionController::Parameters, params.to_hash + assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_hash) + assert_equal({ "crab" => "Senjougahara Hitagi" }, params) + ensure + ActionController::Parameters.permit_all_parameters = false + end end test "to_unsafe_h returns unfiltered params" do - assert @params.to_unsafe_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_unsafe_h.is_a? ActionController::Parameters + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_unsafe_h + assert_not_kind_of ActionController::Parameters, @params.to_unsafe_h end test "to_unsafe_h returns unfiltered params even after accessing few keys" do - params = ActionController::Parameters.new("f"=>{ "language_facet"=>["Tibetan"] }) - expected = { "f"=>{ "language_facet"=>["Tibetan"] } } + params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] }) + expected = { "f" => { "language_facet" => ["Tibetan"] } } - assert params["f"].is_a? ActionController::Parameters + assert_instance_of ActionController::Parameters, params["f"] assert_equal expected, params.to_unsafe_h end + test "to_unsafe_h does not mutate the parameters" do + params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] }) + params[:f] + + params.to_unsafe_h + + assert_not_predicate params, :permitted? + assert_not_predicate params[:f], :permitted? + end + test "to_h only deep dups Ruby collections" do company = Class.new do attr_reader :dupped diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb index 4fb1564c68..6fba2fde91 100644 --- a/actionpack/test/controller/parameters/serialization_test.rb +++ b/actionpack/test/controller/parameters/serialization_test.rb @@ -14,12 +14,10 @@ class ParametersSerializationTest < ActiveSupport::TestCase test "yaml serialization" do params = ActionController::Parameters.new(key: :value) - assert_equal <<-end_of_yaml.strip_heredoc, YAML.dump(params) - --- !ruby/object:ActionController::Parameters - parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess - key: :value - permitted: false - end_of_yaml + yaml_dump = YAML.dump(params) + assert_match("--- !ruby/object:ActionController::Parameters", yaml_dump) + assert_match(/parameters: !ruby\/hash:ActiveSupport::HashWithIndifferentAccess\n\s+key: :value/, yaml_dump) + assert_match("permitted: false", yaml_dump) end test "yaml deserialization" do diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 1549405fe7..2a41d57b26 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -32,6 +32,10 @@ class ParamsWrapperTest < ActionController::TestCase def self.attribute_names [] end + + def self.stored_attributes + { settings: [:color, :size] } + end end class Person @@ -50,7 +54,7 @@ class ParamsWrapperTest < ActionController::TestCase with_default_wrapper_options do @request.env["CONTENT_TYPE"] = "application/json" post :parse, params: { "username" => "sikachu" } - assert_equal @request.filtered_parameters, "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" } + assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" } }, @request.filtered_parameters) end end @@ -62,6 +66,17 @@ class ParamsWrapperTest < ActionController::TestCase end end + def test_store_accessors_wrapped + assert_called(User, :attribute_names, times: 2, returns: ["username"]) do + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "color" => "blue", "size" => "large" } + assert_parameters("username" => "sikachu", "color" => "blue", "size" => "large", + "user" => { "username" => "sikachu", "color" => "blue", "size" => "large" }) + end + end + end + def test_specify_wrapper_name with_default_wrapper_options do UsersController.wrap_parameters :person @@ -212,6 +227,16 @@ class ParamsWrapperTest < ActionController::TestCase ) end end + + def test_handles_empty_content_type + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = nil + _controller_class.dispatch(:parse, @request, @response) + + assert_equal 200, @response.status + assert_equal "", @response.body + end + end end class NamespacedParamsWrapperTest < ActionController::TestCase diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 495e41ce76..5b16af78c4 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -21,8 +21,8 @@ end class RedirectController < ActionController::Base # empty method not used anywhere to ensure methods like # `status` and `location` aren't called on `redirect_to` calls - def status; render plain: "called status"; end - def location; render plain: "called location"; end + def status; raise "Should not be called!"; end + def location; raise "Should not be called!"; end def simple_redirect redirect_to action: "hello_world" @@ -56,10 +56,6 @@ class RedirectController < ActionController::Base redirect_to("/things/stuff", status: 301) end - def redirect_to_back_with_status - redirect_to :back, status: 307 - end - def redirect_back_with_status redirect_back(fallback_location: "/things/stuff", status: 307) end @@ -88,10 +84,6 @@ class RedirectController < ActionController::Base redirect_to "//www.rubyonrails.org/" end - def redirect_to_back - redirect_to :back - end - def redirect_to_existing_record redirect_to Workshop.new(5) end @@ -131,7 +123,7 @@ class RedirectController < ActionController::Base def rescue_errors(e) raise e end - protected + private def dashbord_url(id, message) url_for action: "dashboard", params: { "id" => id, "message" => message } end @@ -206,17 +198,6 @@ class RedirectTest < ActionController::TestCase assert_equal "http://test.host/things/stuff", redirect_to_url end - def test_redirect_to_back_with_status - @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - - assert_deprecated do - get :redirect_to_back_with_status - end - - assert_response 307 - assert_equal "http://www.example.com/coming/from", redirect_to_url - end - def test_simple_redirect_using_options get :host_redirect assert_response :redirect @@ -259,29 +240,6 @@ class RedirectTest < ActionController::TestCase assert_equal "//www.rubyonrails.org/", redirect_to_url end - def test_redirect_to_back - @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - - assert_deprecated do - get :redirect_to_back - end - - assert_response :redirect - assert_equal "http://www.example.com/coming/from", redirect_to_url - end - - def test_redirect_to_back_with_no_referer - assert_raise(ActionController::RedirectBackError) { - @request.env["HTTP_REFERER"] = nil - - assert_deprecated do - get :redirect_to_back - end - - get :redirect_to_back - } - end - def test_redirect_back referer = "http://www.example.com/coming/from" @request.env["HTTP_REFERER"] = referer @@ -327,10 +285,10 @@ class RedirectTest < ActionController::TestCase end def test_redirect_to_params - error = assert_raise(ArgumentError) do + error = assert_raise(ActionController::UnfilteredParameters) do get :redirect_to_params end - assert_equal ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE, error.message + assert_equal "unable to convert unpermitted parameters to hash", error.message end def test_redirect_to_with_block diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 213829bd9e..79552ec8f1 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -5,7 +5,7 @@ require "pathname" class RenderJsonTest < ActionController::TestCase class JsonRenderable - def as_json(options={}) + def as_json(options = {}) hash = { a: :b, c: :d, e: :f } hash.except!(*options[:except]) if options[:except] hash diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 70e5760764..17d834d55f 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -160,10 +160,6 @@ class TestController < ActionController::Base render action: "hello_world" end - def respond_with_empty_body - render nothing: true - end - def conditional_hello_with_bangs render action: "hello_world" end @@ -173,14 +169,6 @@ class TestController < ActionController::Base fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ]) end - def head_with_status_hash - head status: :created - end - - def head_with_hash_does_not_include_status - head warning: :deprecated - end - def head_created head :created end @@ -269,7 +257,7 @@ end module TemplateModificationHelper private def modify_template(name) - path = File.expand_path("../../fixtures/#{name}.erb", __FILE__) + path = File.expand_path("../fixtures/#{name}.erb", __dir__) original = File.read(path) File.write(path, "#{original} Modified!") ActionView::LookupContext::DetailsKey.clear @@ -299,9 +287,9 @@ class ExpiresInRenderTest < ActionController::TestCase def test_dynamic_render_with_file # This is extremely bad, but should be possible to do. - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' } - assert_equal File.read(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")), + assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)), response.body end @@ -318,17 +306,16 @@ class ExpiresInRenderTest < ActionController::TestCase end def test_dynamic_render - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) assert_raises ActionView::MissingTemplate do get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' } end end def test_permitted_dynamic_render_file_hash - skip "FIXME: this test passes on 4-2-stable but not master. Why?" - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } } - assert_equal File.read(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")), + assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)), response.body end @@ -379,14 +366,8 @@ class ExpiresInRenderTest < ActionController::TestCase assert_match(/no-transform/, @response.headers["Cache-Control"]) end - def test_render_nothing_deprecated - assert_deprecated do - get :respond_with_empty_body - end - end - def test_date_header_when_expires_in - time = Time.mktime(2011,10,30) + time = Time.mktime(2011, 10, 30) Time.stub :now, time do get :conditional_hello_with_expires_in assert_equal Time.now.httpdate, @response.headers["Date"] @@ -670,19 +651,6 @@ class HeadRenderTest < ActionController::TestCase assert_response :created end - def test_passing_hash_to_head_as_first_parameter_deprecated - assert_deprecated do - get :head_with_status_hash - end - end - - def test_head_with_default_value_is_deprecated - assert_deprecated do - get :head_with_hash_does_not_include_status - assert_response :ok - end - end - def test_head_created_with_application_json_content_type post :head_created_with_application_json_content_type assert @response.body.blank? diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb index d6f09f2d90..052c974d68 100644 --- a/actionpack/test/controller/renderer_test.rb +++ b/actionpack/test/controller/renderer_test.rb @@ -19,6 +19,16 @@ class RendererTest < ActiveSupport::TestCase assert_equal controller, renderer.controller end + test "creating with new defaults" do + renderer = ApplicationController.renderer + + new_defaults = { https: true } + new_renderer = renderer.with_defaults(new_defaults).new + content = new_renderer.render(inline: "<%= request.ssl? %>") + + assert_equal "true", content + end + test "rendering with a class renderer" do renderer = ApplicationController.renderer content = renderer.render template: "ruby_template" @@ -60,6 +70,14 @@ class RendererTest < ActiveSupport::TestCase assert_equal "true", content end + test "rendering with custom env using a key that is not in RACK_KEY_TRANSLATION" do + value = "warden is here" + renderer = ApplicationController.renderer.new warden: value + content = renderer.render inline: "<%= request.env['warden'] %>" + + assert_equal value, content + end + test "rendering with defaults" do renderer = ApplicationController.renderer.new https: true content = renderer.render inline: "<%= request.ssl? %>" @@ -95,6 +113,20 @@ class RendererTest < ActiveSupport::TestCase assert_equal "true", content end + test "return valid asset url with defaults" do + renderer = ApplicationController.renderer + content = renderer.render inline: "<%= asset_url 'asset.jpg' %>" + + assert_equal "http://example.org/asset.jpg", content + end + + test "return valid asset url when https is true" do + renderer = ApplicationController.renderer.new https: true + content = renderer.render inline: "<%= asset_url 'asset.jpg' %>" + + assert_equal "https://example.org/asset.jpg", content + end + private def render @render ||= ApplicationController.renderer.method(:render) diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb index 122f5be549..ccc700d79c 100644 --- a/actionpack/test/controller/renderers_test.rb +++ b/actionpack/test/controller/renderers_test.rb @@ -10,7 +10,7 @@ class RenderersTest < ActionController::TestCase end end class JsonRenderable - def as_json(options={}) + def as_json(options = {}) hash = { a: :b, c: :d, e: :f } hash.except!(*options[:except]) if options[:except] hash diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index fa049fbc9f..1440db00f6 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -8,19 +8,33 @@ class ActionController::TestRequestTest < ActionController::TestCase def test_mutating_session_options_does_not_affect_default_options @request.session_options[:myparam] = 123 - assert_equal nil, ActionController::TestSession::DEFAULT_OPTIONS[:myparam] + assert_nil ActionController::TestSession::DEFAULT_OPTIONS[:myparam] end - ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_key do |option| - test "rack default session options #{option} exists in session options and is default" do - assert_equal(ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS[option], - @request.session_options[option], - "Missing rack session default option #{option} in request.session_options") + def test_content_length_has_bytes_count_value + non_ascii_parameters = { data: { content: "Latin + Кириллица" } } + @request.set_header "REQUEST_METHOD", "POST" + @request.set_header "CONTENT_TYPE", "application/json" + @request.assign_parameters(@routes, "test", "create", non_ascii_parameters, + "/test", [:data, :controller, :action]) + assert_equal(StringIO.new(non_ascii_parameters.to_json).length.to_s, + @request.get_header("CONTENT_LENGTH")) + end + + ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_pair do |key, value| + test "rack default session options #{key} exists in session options and is default" do + if value.nil? + assert_nil(@request.session_options[key], + "Missing rack session default option #{key} in request.session_options") + else + assert_equal(value, @request.session_options[key], + "Missing rack session default option #{key} in request.session_options") + end end - test "rack default session options #{option} exists in session options" do - assert(@request.session_options.has_key?(option), - "Missing rack session option #{option} in request.session_options") + test "rack default session options #{key} exists in session options" do + assert(@request.session_options.has_key?(key), + "Missing rack session option #{key} in request.session_options") end end end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 90d5ab3c67..521d93f02e 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -35,6 +35,22 @@ module RequestForgeryProtectionActions render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>" end + def form_with_remote + render inline: "<%= form_with(scope: :some_resource) {} %>" + end + + def form_with_remote_with_token + render inline: "<%= form_with(scope: :some_resource, authenticity_token: true) {} %>" + end + + def form_with_local_with_token + render inline: "<%= form_with(scope: :some_resource, local: true, authenticity_token: true) {} %>" + end + + def form_with_remote_with_external_token + render inline: "<%= form_with(scope: :some_resource, authenticity_token: 'external_token') {} %>" + end + def same_origin_js render js: "foo();" end @@ -92,7 +108,7 @@ class PrependProtectForgeryBaseController < ActionController::Base render inline: "OK" end - protected + private def add_called_callback(name) @called_callbacks ||= [] @@ -235,6 +251,80 @@ module RequestForgeryProtectionTests end end + def test_should_render_form_with_with_token_tag_if_remote + assert_not_blocked do + get :form_with_remote + end + assert_match(/authenticity_token/, response.body) + end + + def test_should_render_form_with_without_token_tag_if_remote_and_embedding_token_is_off + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = false + assert_not_blocked do + get :form_with_remote + end + assert_no_match(/authenticity_token/, response.body) + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + assert_not_blocked do + get :form_with_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested + assert_not_blocked do + get :form_with_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + end + + def test_should_render_form_with_with_token_tag_if_remote_and_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_remote_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_with_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_local_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_embedding_token_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_remote + end + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + def test_should_allow_get assert_not_blocked { get :index } end @@ -347,6 +437,10 @@ module RequestForgeryProtectionTests end def test_should_block_post_with_origin_checking_and_wrong_origin + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + forgery_protection_origin_check do session[:_csrf_token] = @token @controller.stub :form_authenticity_token, @token do @@ -356,6 +450,13 @@ module RequestForgeryProtectionTests end end end + + assert_match( + "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)", + logger.logged(:warn).last + ) + ensure + ActionController::Base.logger = old_logger end def test_should_warn_on_missing_csrf_token diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index 9fa2b6dbb0..46bb374b3f 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -72,15 +72,27 @@ class ParametersRequireTest < ActiveSupport::TestCase assert params.value?("cinco") end - test "Deprecated methods are deprecated" do - assert_deprecated do - ActionController::Parameters.new(foo: "bar").merge!(bar: "foo") + test "to_param works like in a Hash" do + params = ActionController::Parameters.new(nested: { key: "value" }).permit! + assert_equal({ nested: { key: "value" } }.to_param, params.to_param) + + params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! } + assert_equal({ root: { nested: { key: "value" } } }.to_param, params.to_param) + + assert_raise(ActionController::UnfilteredParameters) do + ActionController::Parameters.new(nested: { key: "value" }).to_param end end - test "to_query is not supported" do - assert_deprecated do - ActionController::Parameters.new(foo: "bar").to_param + test "to_query works like in a Hash" do + params = ActionController::Parameters.new(nested: { key: "value" }).permit! + assert_equal({ nested: { key: "value" } }.to_query, params.to_query) + + params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! } + assert_equal({ root: { nested: { key: "value" } } }.to_query, params.to_query) + + assert_raise(ActionController::UnfilteredParameters) do + ActionController::Parameters.new(nested: { key: "value" }).to_query end end end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index a98e6479b7..9ae22c4554 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -149,7 +149,7 @@ class RescueController < ActionController::Base raise RangeError end - protected + private def deny_access head :forbidden end @@ -327,7 +327,7 @@ class RescueTest < ActionDispatch::IntegrationTest raise "b00m" end - protected + private def show_errors(exception) render plain: exception.message end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index b572e7e8d5..fad34dacce 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -27,7 +27,7 @@ class ResourcesTest < ActionController::TestCase def test_override_paths_for_member_and_collection_methods collection_methods = { rss: :get, reorder: :post, csv: :post } - member_methods = { rss: :get, atom: :get, upload: :post, fix: :post } + member_methods = { rss: :get, atom: :get, upload: :post, fix: :post } path_names = { new: "nuevo", rss: "canal", fix: "corrigir" } with_restful_routing :messages, @@ -792,7 +792,7 @@ class ResourcesTest < ActionController::TestCase end assert_simply_restful_for :product_reviews, controller: "messages", as: "reviews", name_prefix: "product_", path_prefix: "products/1/", options: { product_id: "1" } - assert_simply_restful_for :tutor_reviews,controller: "comments", as: "reviews", name_prefix: "tutor_", path_prefix: "tutors/1/", options: { tutor_id: "1" } + assert_simply_restful_for :tutor_reviews, controller: "comments", as: "reviews", name_prefix: "tutor_", path_prefix: "tutors/1/", options: { tutor_id: "1" } end end @@ -1089,7 +1089,7 @@ class ResourcesTest < ActionController::TestCase end end - protected + private def with_restful_routing(*args) options = args.extract_options! collection_methods = options.delete(:collection) @@ -1306,7 +1306,7 @@ class ResourcesTest < ActionController::TestCase end def assert_named_route(expected, route, options) - actual = @controller.send(route, options) rescue $!.class.name + actual = @controller.send(route, options) rescue $!.class.name assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})" end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 7be2ad2b28..56b39510bb 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -91,7 +91,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg")) - assert_equal({ "artist"=>"journey", "song"=>"faithfully" }, hash) + assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash) end def test_id_with_dash @@ -103,7 +103,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg")) - assert_equal({ "id"=>"faithfully-omg" }, hash) + assert_equal({ "id" => "faithfully-omg" }, hash) end def test_dash_with_custom_regexp @@ -115,7 +115,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/123-omg")) - assert_equal({ "artist"=>"journey", "song"=>"123" }, hash) + assert_equal({ "artist" => "journey", "song" => "123" }, hash) assert_equal "Not Found", get(URI("http://example.org/journey/faithfully-omg")) end @@ -128,7 +128,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-faithfully")) - assert_equal({ "artist"=>"journey", "song"=>"faithfully" }, hash) + assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash) end def test_pre_dash_with_custom_regexp @@ -140,7 +140,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-123")) - assert_equal({ "artist"=>"journey", "song"=>"123" }, hash) + assert_equal({ "artist" => "journey", "song" => "123" }, hash) assert_equal "Not Found", get(URI("http://example.org/journey/omg-faithfully")) end @@ -1931,7 +1931,7 @@ class RackMountIntegrationTests < ActiveSupport::TestCase get "clients" => "projects#index" get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i - get "extended/geocode/:postalcode" => "geocode#show",:constraints => { + get "extended/geocode/:postalcode" => "geocode#show", :constraints => { postalcode: /# Postcode format \d{5} #Prefix (-\d{4})? #Suffix diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 8dc565ac8d..e265c6c49c 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" module TestFileUtils def file_name() File.basename(__FILE__) end - def file_path() File.expand_path(__FILE__) end + def file_path() __FILE__ end def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end end @@ -180,7 +180,7 @@ class SendFileTest < ActionController::TestCase "file.zip" => "application/zip", "file.unk" => "application/octet-stream", "zip" => "application/octet-stream" - }.each do |filename,expected_type| + }.each do |filename, expected_type| get __method__, params: { filename: filename } assert_equal expected_type, response.content_type end @@ -241,10 +241,17 @@ class SendFileTest < ActionController::TestCase assert_equal "text/calendar; charset=utf-8", response.headers["Content-Type"] end + def test_send_file_charset_with_type_options_key_without_charset + @controller = SendFileWithActionControllerLive.new + @controller.options = { type: "image/png" } + response = process("file") + assert_equal "image/png", response.headers["Content-Type"] + end + def test_send_file_charset_with_content_type_options_key @controller = SendFileWithActionControllerLive.new @controller.options = { content_type: "text/calendar" } response = process("file") - assert_equal "text/calendar; charset=utf-8", response.headers["Content-Type"] + assert_equal "text/calendar", response.headers["Content-Type"] end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 738d8bab6d..677e2ddded 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -100,11 +100,11 @@ HTML end def test_xml_output - response.content_type = "application/xml" + response.content_type = params[:response_as] render plain: <<XML <?xml version="1.0" encoding="UTF-8"?> <root> - <area>area is an empty tag in HTML, raising an error if not in xml mode</area> + <area><p>area is an empty tag in HTML, so it won't contain this content</p></area> </root> XML end @@ -122,7 +122,7 @@ XML end def test_send_file - send_file(File.expand_path(__FILE__)) + send_file(__FILE__) end def redirect_to_same_controller @@ -134,7 +134,7 @@ XML end def create - head :created, location: "created resource" + head :created, location: "/resource" end def render_cookie @@ -229,17 +229,9 @@ XML assert_equal params.to_query, @response.body end - def test_deprecated_body_stream - params = Hash[:page, { name: "page name" }, "some key", 123] - - assert_deprecated { post :render_body, params.dup } - - assert_equal params.to_query, @response.body - end - def test_document_body_and_params_with_post post :test_params, params: { id: 1 } - assert_equal({ "id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params" }, ::JSON.parse(@response.body)) + assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) end def test_document_body_with_post @@ -247,21 +239,11 @@ XML assert_equal "document body", @response.body end - def test_deprecated_document_body_with_post - assert_deprecated { post :render_body, "document body" } - assert_equal "document body", @response.body - end - def test_document_body_with_put put :render_body, body: "document body" assert_equal "document body", @response.body end - def test_deprecated_document_body_with_put - assert_deprecated { put :render_body, "document body" } - assert_equal "document body", @response.body - end - def test_head head :test_params assert_equal 200, @response.status @@ -272,11 +254,6 @@ XML assert_equal "><", flash["test"] end - def test_deprecated_process_with_flash - assert_deprecated { process :set_flash, "GET", nil, nil, "test" => "value" } - assert_equal ">value<", flash["test"] - end - def test_process_with_flash process :set_flash, method: "GET", @@ -284,11 +261,6 @@ XML assert_equal ">value<", flash["test"] end - def test_deprecated_process_with_flash_now - assert_deprecated { process :set_flash_now, "GET", nil, nil, "test_now" => "value_now" } - assert_equal ">value_now<", flash["test_now"] - end - def test_process_with_flash_now process :set_flash_now, method: "GET", @@ -311,14 +283,6 @@ XML assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access" end - def test_process_with_session_arg - assert_deprecated { process :no_op, "GET", nil, "string" => "value1", symbol: "value2" } - assert_equal "value1", session["string"] - assert_equal "value1", session[:string] - assert_equal "value2", session["symbol"] - assert_equal "value2", session[:symbol] - end - def test_process_with_session_kwarg process :no_op, method: "GET", session: { "string" => "value1", symbol: "value2" } assert_equal "value1", session["string"] @@ -327,15 +291,6 @@ XML assert_equal "value2", session[:symbol] end - def test_deprecated_process_merges_session_arg - session[:foo] = "bar" - assert_deprecated { - get :no_op, nil, bar: "baz" - } - assert_equal "bar", session[:foo] - assert_equal "baz", session[:bar] - end - def test_process_merges_session_arg session[:foo] = "bar" get :no_op, session: { bar: "baz" } @@ -343,15 +298,6 @@ XML assert_equal "baz", session[:bar] end - def test_deprecated_merged_session_arg_is_retained_across_requests - assert_deprecated { - get :no_op, nil, foo: "bar" - } - assert_equal "bar", session[:foo] - get :no_op - assert_equal "bar", session[:foo] - end - def test_merged_session_arg_is_retained_across_requests get :no_op, session: { foo: "bar" } assert_equal "bar", session[:foo] @@ -393,11 +339,6 @@ XML assert_equal "/test_case_test/test/test_uri", @response.body end - def test_deprecated_process_with_request_uri_with_params - assert_deprecated { process :test_uri, "GET", id: 7 } - assert_equal "/test_case_test/test/test_uri/7", @response.body - end - def test_process_with_request_uri_with_params process :test_uri, method: "GET", @@ -406,12 +347,6 @@ XML assert_equal "/test_case_test/test/test_uri/7", @response.body end - def test_deprecated_process_with_request_uri_with_params_with_explicit_uri - @request.env["PATH_INFO"] = "/explicit/uri" - assert_deprecated { process :test_uri, "GET", id: 7 } - assert_equal "/explicit/uri", @response.body - end - def test_process_with_request_uri_with_params_with_explicit_uri @request.env["PATH_INFO"] = "/explicit/uri" process :test_uri, method: "GET", params: { id: 7 } @@ -439,18 +374,18 @@ XML assert_equal "OK", @response.body end - def test_should_not_impose_childless_html_tags_in_xml - process :test_xml_output + def test_should_impose_childless_html_tags_in_html + process :test_xml_output, params: { response_as: "text/html" } - begin - $stderr = StringIO.new - assert_select "area" #This will cause a warning if content is processed as HTML - $stderr.rewind && err = $stderr.read - ensure - $stderr = STDERR - end + # <area> auto-closes, so the <p> becomes a sibling + assert_select "root > area + p" + end + + def test_should_not_impose_childless_html_tags_in_xml + process :test_xml_output, params: { response_as: "application/xml" } - assert err.empty? + # <area> is not special, so the <p> is its child + assert_select "root > area > p" end def test_assert_generates @@ -491,20 +426,6 @@ XML end end - def test_deprecated_params_passing - assert_deprecated { - get :test_params, page: { name: "Page name", month: "4", year: "2004", day: "6" } - } - parsed_params = ::JSON.parse(@response.body) - assert_equal( - { - "controller" => "test_case_test/test", "action" => "test_params", - "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } - }, - parsed_params - ) - end - def test_params_passing get :test_params, params: { page: { @@ -589,16 +510,6 @@ XML ) end - def test_deprecated_params_passing_path_parameter_is_string_when_not_html_request - assert_deprecated { get :test_params, format: "json", id: 1 } - parsed_params = ::JSON.parse(@response.body) - assert_equal( - { "controller" => "test_case_test/test", "action" => "test_params", - "format" => "json", "id" => "1" }, - parsed_params - ) - end - def test_params_passing_with_frozen_values assert_nothing_raised do get :test_params, params: { @@ -646,6 +557,11 @@ XML assert_equal 2, @request.request_parameters[:num_value] end + def test_using_as_json_sets_format_json + post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json + assert_equal "json", @request.format + end + def test_mutating_content_type_headers_for_plain_text_files_sets_the_header @request.headers["Content-Type"] = "text/plain" post :render_body, params: { name: "foo.txt" } @@ -678,11 +594,6 @@ XML assert_kind_of String, @request.path_parameters[:id] end - def test_deprecared_id_converted_to_string - assert_deprecated { get :test_params, id: 20, foo: Object.new } - assert_kind_of String, @request.path_parameters[:id] - end - def test_array_path_parameter_handled_properly with_routing do |set| set.draw do @@ -737,16 +648,10 @@ XML assert_nil @request.env["HTTP_ACCEPT"] end - def test_deprecated_xhr_with_params - assert_deprecated { xhr :get, :test_params, params: { id: 1 } } - - assert_equal({ "id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params" }, ::JSON.parse(@response.body)) - end - def test_xhr_with_params get :test_params, params: { id: 1 }, xhr: true - assert_equal({ "id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params" }, ::JSON.parse(@response.body)) + assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) end def test_xhr_with_session @@ -758,23 +663,6 @@ XML assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access" end - def test_deprecated_xhr_with_session - assert_deprecated { xhr :get, :set_session } - - assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key" - assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access" - assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access" - assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access" - end - - def test_deprecated_params_reset_between_post_requests - assert_deprecated { post :no_op, foo: "bar" } - assert_equal "bar", @request.params[:foo] - - post :no_op - assert @request.params[:foo].blank? - end - def test_params_reset_between_post_requests post :no_op, params: { foo: "bar" } assert_equal "bar", @request.params[:foo] @@ -791,6 +679,11 @@ XML assert_equal "baz", @request.filtered_parameters[:foo] end + def test_path_is_kept_after_the_request + get :test_params, params: { id: "foo" } + assert_equal "/test_case_test/test/test_params/foo", @request.path + end + def test_path_params_reset_between_request get :test_params, params: { id: "foo" } assert_equal "foo", @request.path_parameters[:id] @@ -865,10 +758,10 @@ XML def test_multiple_mixed_method_process_should_scrub_rack_input post :test_params, params: { id: 1, foo: "an foo" } - assert_equal({ "id"=>"1", "foo" => "an foo", "controller"=>"test_case_test/test", "action"=>"test_params" }, ::JSON.parse(@response.body)) + assert_equal({ "id" => "1", "foo" => "an foo", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) get :test_params, params: { bar: "an bar" } - assert_equal({ "bar"=>"an bar", "controller"=>"test_case_test/test", "action"=>"test_params" }, ::JSON.parse(@response.body)) + assert_equal({ "bar" => "an bar", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) end %w(controller response request).each do |variable| @@ -887,13 +780,13 @@ XML end end - FILES_DIR = File.dirname(__FILE__) + "/../fixtures/multipart" + FILES_DIR = File.expand_path("../fixtures/multipart", __dir__) READ_BINARY = "rb:binary" READ_PLAIN = "r:binary" def test_test_uploaded_file - filename = "mona_lisa.jpg" + filename = "ruby_on_rails.jpg" path = "#{FILES_DIR}/#{filename}" content_type = "image/png" expected = File.read(path) @@ -908,18 +801,17 @@ XML new_content_type = "new content_type" file.content_type = new_content_type assert_equal new_content_type, file.content_type - end def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case TestCaseTest.stub :fixture_path, FILES_DIR do - uploaded_file = fixture_file_upload("/mona_lisa.jpg", "image/png") - assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + uploaded_file = fixture_file_upload("/ruby_on_rails.jpg", "image/png") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end end def test_test_uploaded_file_with_binary - filename = "mona_lisa.jpg" + filename = "ruby_on_rails.jpg" path = "#{FILES_DIR}/#{filename}" content_type = "image/png" @@ -931,7 +823,7 @@ XML end def test_fixture_file_upload_with_binary - filename = "mona_lisa.jpg" + filename = "ruby_on_rails.jpg" path = "#{FILES_DIR}/#{filename}" content_type = "image/jpg" @@ -943,53 +835,44 @@ XML end def test_fixture_file_upload_should_be_able_access_to_tempfile - file = fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + 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" end def test_fixture_file_upload post :test_file_upload, params: { - file: fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg") } - assert_equal "159528", @response.body + assert_equal "45142", @response.body end def test_fixture_file_upload_relative_to_fixture_path TestCaseTest.stub :fixture_path, FILES_DIR do - uploaded_file = fixture_file_upload("mona_lisa.jpg", "image/jpg") - assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + uploaded_file = fixture_file_upload("ruby_on_rails.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end end def test_fixture_file_upload_ignores_fixture_path_given_full_path - TestCaseTest.stub :fixture_path, File.dirname(__FILE__) do - uploaded_file = fixture_file_upload("#{FILES_DIR}/mona_lisa.jpg", "image/jpg") - assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + TestCaseTest.stub :fixture_path, __dir__ do + uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end end def test_fixture_file_upload_ignores_nil_fixture_path - uploaded_file = fixture_file_upload("#{FILES_DIR}/mona_lisa.jpg", "image/jpg") - assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read - end - - def test_deprecated_action_dispatch_uploaded_file_upload - filename = "mona_lisa.jpg" - path = "#{FILES_DIR}/#{filename}" - assert_deprecated { - post :test_file_upload, file: Rack::Test::UploadedFile.new(path, "image/jpg", true) - } - assert_equal "159528", @response.body + uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end def test_action_dispatch_uploaded_file_upload - filename = "mona_lisa.jpg" + filename = "ruby_on_rails.jpg" path = "#{FILES_DIR}/#{filename}" post :test_file_upload, params: { file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } - assert_equal "159528", @response.body + assert_equal "45142", @response.body end def test_test_uploaded_file_exception_when_file_doesnt_exist @@ -1001,12 +884,12 @@ XML assert_response :created # Redirect url doesn't care that it wasn't a :redirect response. - assert_equal "created resource", @response.redirect_url + assert_equal "/resource", @response.redirect_url assert_equal @response.redirect_url, redirect_to_url # Must be a :redirect response. assert_raise(ActiveSupport::TestCase::Assertion) do - assert_redirected_to "created resource" + assert_redirected_to "/resource" end end diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index c7ea6c5ef6..f640e77b99 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -43,7 +43,7 @@ module ActionPack get "clients" => "projects#index" get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i - get "extended/geocode/:postalcode" => "geocode#show",:constraints => { + get "extended/geocode/:postalcode" => "geocode#show", :constraints => { postalcode: /# Postcode format \d{5} #Prefix (-\d{4})? #Suffix @@ -73,99 +73,99 @@ module ActionPack end [ - ["/admin/users",[ { use_route: "admin_users" }]], - ["/admin/users",[ { controller: "admin/users" }]], - ["/admin/users",[ { controller: "admin/users", action: "index" }]], - ["/admin/users",[ { action: "index" }, { controller: "admin/users", action: "index" }, "/admin/users"]], - ["/admin/users",[ { controller: "users", action: "index" }, { controller: "admin/accounts", action: "show", id: "1" }, "/admin/accounts/show/1"]], - ["/people",[ { controller: "/people", action: "index" }, { controller: "admin/accounts", action: "foo", id: "bar" }, "/admin/accounts/foo/bar"]], - - ["/admin/posts",[ { controller: "admin/posts" }]], - ["/admin/posts/new",[ { controller: "admin/posts", action: "new" }]], - - ["/blog/2009",[ { controller: "posts", action: "show_date", year: 2009 }]], - ["/blog/2009/1",[ { controller: "posts", action: "show_date", year: 2009, month: 1 }]], - ["/blog/2009/1/1",[ { controller: "posts", action: "show_date", year: 2009, month: 1, day: 1 }]], - - ["/archive/2010",[ { controller: "archive", action: "index", year: "2010" }]], - ["/archive",[ { controller: "archive", action: "index" }]], - ["/archive?year=january",[ { controller: "archive", action: "index", year: "january" }]], - - ["/people",[ { controller: "people", action: "index" }]], - ["/people",[ { action: "index" }, { controller: "people", action: "index" }, "/people"]], - ["/people",[ { action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], - ["/people",[ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], - ["/people",[ {}, { controller: "people", action: "index" }, "/people"]], - ["/people/1",[ { controller: "people", action: "show" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], - ["/people/new",[ { use_route: "new_person" }]], - ["/people/new",[ { controller: "people", action: "new" }]], - ["/people/1",[ { use_route: "person", id: "1" }]], - ["/people/1",[ { controller: "people", action: "show", id: "1" }]], - ["/people/1.xml",[ { controller: "people", action: "show", id: "1", format: "xml" }]], - ["/people/1",[ { controller: "people", action: "show", id: 1 }]], - ["/people/1",[ { controller: "people", action: "show", id: Model.new("1") }]], - ["/people/1",[ { action: "show", id: "1" }, { controller: "people", action: "index" }, "/people"]], - ["/people/1",[ { action: "show", id: 1 }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], - ["/people",[ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], - ["/people/1",[ {}, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], - ["/people/1",[ { controller: "people", action: "show" }, { controller: "people", action: "index", id: "1" }, "/people/index/1"]], - ["/people/1/edit",[ { controller: "people", action: "edit", id: "1" }]], - ["/people/1/edit.xml",[ { controller: "people", action: "edit", id: "1", format: "xml" }]], - ["/people/1/edit",[ { use_route: "edit_person", id: "1" }]], - ["/people/1?legacy=true",[ { controller: "people", action: "show", id: "1", legacy: "true" }]], - ["/people?legacy=true",[ { controller: "people", action: "index", legacy: "true" }]], - - ["/id_default/2",[ { controller: "foo", action: "id_default", id: "2" }]], - ["/id_default",[ { controller: "foo", action: "id_default", id: "1" }]], - ["/id_default",[ { controller: "foo", action: "id_default", id: 1 }]], - ["/id_default",[ { controller: "foo", action: "id_default" }]], - ["/optional/bar",[ { controller: "posts", action: "index", optional: "bar" }]], - ["/posts",[ { controller: "posts", action: "index" }]], - - ["/project",[ { controller: "project", action: "index" }]], - ["/projects/1",[ { controller: "project", action: "index", project_id: "1" }]], - ["/projects/1",[ { controller: "project", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]], - ["/projects/1",[ { use_route: "project", controller: "project", action: "index", project_id: "1" }]], - ["/projects/1",[ { use_route: "project", controller: "project", action: "index" }, { controller: "project", action: "index", project_id: "1" }, "/projects/1"]], - - ["/clients",[ { controller: "projects", action: "index" }]], - ["/clients?project_id=1",[ { controller: "projects", action: "index", project_id: "1" }]], - ["/clients",[ { controller: "projects", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]], - - ["/comment/20",[ { id: 20 }, { controller: "comments", action: "show" }, "/comments/show"]], - ["/comment/20",[ { controller: "comments", id: 20, action: "show" }]], - ["/comments/boo",[ { controller: "comments", action: "boo" }]], - - ["/ws/posts/show/1",[ { controller: "posts", action: "show", id: "1", ws: true }]], - ["/ws/posts",[ { controller: "posts", action: "index", ws: true }]], - - ["/account",[ { controller: "account", action: "subscription" }]], - ["/account/billing",[ { controller: "account", action: "billing" }]], - - ["/pages/1/notes/show/1",[ { page_id: "1", controller: "notes", action: "show", id: "1" }]], - ["/pages/1/notes/list",[ { page_id: "1", controller: "notes", action: "list" }]], - ["/pages/1/notes",[ { page_id: "1", controller: "notes", action: "index" }]], - ["/pages/1/notes",[ { page_id: "1", controller: "notes" }]], - ["/notes",[ { page_id: nil, controller: "notes" }]], - ["/notes",[ { controller: "notes" }]], - ["/notes/print",[ { controller: "notes", action: "print" }]], - ["/notes/print",[ {}, { controller: "notes", action: "print" }, "/notes/print"]], - - ["/notes/index/1",[ { controller: "notes" }, { controller: "notes", action: "index", id: "1" }, "/notes/index/1"]], - ["/notes/index/1",[ { controller: "notes" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], - ["/notes/index/1",[ { action: "index" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], - ["/notes/index/1",[ {}, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], - ["/notes/show/1",[ {}, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], - ["/posts",[ { controller: "posts" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], - ["/notes/list",[ { action: "list" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], - - ["/posts/ping",[ { controller: "posts", action: "ping" }]], - ["/posts/show/1",[ { controller: "posts", action: "show", id: "1" }]], - ["/posts/show/1",[ { controller: "posts", action: "show", id: "1", format: "" }]], - ["/posts",[ { controller: "posts" }]], - ["/posts",[ { controller: "posts", action: "index" }]], - ["/posts/create",[ { action: "create" }, { day: nil, month: nil, controller: "posts", action: "show_date" }, "/blog"]], - ["/posts?foo=bar",[ { controller: "posts", foo: "bar" }]], + ["/admin/users", [ { use_route: "admin_users" }]], + ["/admin/users", [ { controller: "admin/users" }]], + ["/admin/users", [ { controller: "admin/users", action: "index" }]], + ["/admin/users", [ { action: "index" }, { controller: "admin/users", action: "index" }, "/admin/users"]], + ["/admin/users", [ { controller: "users", action: "index" }, { controller: "admin/accounts", action: "show", id: "1" }, "/admin/accounts/show/1"]], + ["/people", [ { controller: "/people", action: "index" }, { controller: "admin/accounts", action: "foo", id: "bar" }, "/admin/accounts/foo/bar"]], + + ["/admin/posts", [ { controller: "admin/posts" }]], + ["/admin/posts/new", [ { controller: "admin/posts", action: "new" }]], + + ["/blog/2009", [ { controller: "posts", action: "show_date", year: 2009 }]], + ["/blog/2009/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1 }]], + ["/blog/2009/1/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1, day: 1 }]], + + ["/archive/2010", [ { controller: "archive", action: "index", year: "2010" }]], + ["/archive", [ { controller: "archive", action: "index" }]], + ["/archive?year=january", [ { controller: "archive", action: "index", year: "january" }]], + + ["/people", [ { controller: "people", action: "index" }]], + ["/people", [ { action: "index" }, { controller: "people", action: "index" }, "/people"]], + ["/people", [ { action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people", [ {}, { controller: "people", action: "index" }, "/people"]], + ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people/new", [ { use_route: "new_person" }]], + ["/people/new", [ { controller: "people", action: "new" }]], + ["/people/1", [ { use_route: "person", id: "1" }]], + ["/people/1", [ { controller: "people", action: "show", id: "1" }]], + ["/people/1.xml", [ { controller: "people", action: "show", id: "1", format: "xml" }]], + ["/people/1", [ { controller: "people", action: "show", id: 1 }]], + ["/people/1", [ { controller: "people", action: "show", id: Model.new("1") }]], + ["/people/1", [ { action: "show", id: "1" }, { controller: "people", action: "index" }, "/people"]], + ["/people/1", [ { action: "show", id: 1 }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people/1", [ {}, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "index", id: "1" }, "/people/index/1"]], + ["/people/1/edit", [ { controller: "people", action: "edit", id: "1" }]], + ["/people/1/edit.xml", [ { controller: "people", action: "edit", id: "1", format: "xml" }]], + ["/people/1/edit", [ { use_route: "edit_person", id: "1" }]], + ["/people/1?legacy=true", [ { controller: "people", action: "show", id: "1", legacy: "true" }]], + ["/people?legacy=true", [ { controller: "people", action: "index", legacy: "true" }]], + + ["/id_default/2", [ { controller: "foo", action: "id_default", id: "2" }]], + ["/id_default", [ { controller: "foo", action: "id_default", id: "1" }]], + ["/id_default", [ { controller: "foo", action: "id_default", id: 1 }]], + ["/id_default", [ { controller: "foo", action: "id_default" }]], + ["/optional/bar", [ { controller: "posts", action: "index", optional: "bar" }]], + ["/posts", [ { controller: "posts", action: "index" }]], + + ["/project", [ { controller: "project", action: "index" }]], + ["/projects/1", [ { controller: "project", action: "index", project_id: "1" }]], + ["/projects/1", [ { controller: "project", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]], + ["/projects/1", [ { use_route: "project", controller: "project", action: "index", project_id: "1" }]], + ["/projects/1", [ { use_route: "project", controller: "project", action: "index" }, { controller: "project", action: "index", project_id: "1" }, "/projects/1"]], + + ["/clients", [ { controller: "projects", action: "index" }]], + ["/clients?project_id=1", [ { controller: "projects", action: "index", project_id: "1" }]], + ["/clients", [ { controller: "projects", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]], + + ["/comment/20", [ { id: 20 }, { controller: "comments", action: "show" }, "/comments/show"]], + ["/comment/20", [ { controller: "comments", id: 20, action: "show" }]], + ["/comments/boo", [ { controller: "comments", action: "boo" }]], + + ["/ws/posts/show/1", [ { controller: "posts", action: "show", id: "1", ws: true }]], + ["/ws/posts", [ { controller: "posts", action: "index", ws: true }]], + + ["/account", [ { controller: "account", action: "subscription" }]], + ["/account/billing", [ { controller: "account", action: "billing" }]], + + ["/pages/1/notes/show/1", [ { page_id: "1", controller: "notes", action: "show", id: "1" }]], + ["/pages/1/notes/list", [ { page_id: "1", controller: "notes", action: "list" }]], + ["/pages/1/notes", [ { page_id: "1", controller: "notes", action: "index" }]], + ["/pages/1/notes", [ { page_id: "1", controller: "notes" }]], + ["/notes", [ { page_id: nil, controller: "notes" }]], + ["/notes", [ { controller: "notes" }]], + ["/notes/print", [ { controller: "notes", action: "print" }]], + ["/notes/print", [ {}, { controller: "notes", action: "print" }, "/notes/print"]], + + ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", action: "index", id: "1" }, "/notes/index/1"]], + ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], + ["/notes/index/1", [ { action: "index" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], + ["/notes/index/1", [ {}, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], + ["/notes/show/1", [ {}, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], + ["/posts", [ { controller: "posts" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], + ["/notes/list", [ { action: "list" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], + + ["/posts/ping", [ { controller: "posts", action: "ping" }]], + ["/posts/show/1", [ { controller: "posts", action: "show", id: "1" }]], + ["/posts/show/1", [ { controller: "posts", action: "show", id: "1", format: "" }]], + ["/posts", [ { controller: "posts" }]], + ["/posts", [ { controller: "posts", action: "index" }]], + ["/posts/create", [ { action: "create" }, { day: nil, month: nil, controller: "posts", action: "show_date" }, "/blog"]], + ["/posts?foo=bar", [ { controller: "posts", foo: "bar" }]], ["/posts?foo%5B%5D=bar&foo%5B%5D=baz", [{ controller: "posts", foo: ["bar", "baz"] }]], ["/posts?page=2", [{ controller: "posts", page: 2 }]], ["/posts?q%5Bfoo%5D%5Ba%5D=b", [{ controller: "posts", q: { foo: { a: "b" } } }]], diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 8d7190365d..2afe67ed91 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -26,7 +26,7 @@ module AbstractController action: :index } }.url_helpers - self.default_url_options[:host] = "example.com" + default_url_options[:host] = "example.com" } path = klass.new.fun_path(controller: :articles, @@ -223,13 +223,13 @@ module AbstractController def test_trailing_slash add_host! options = { controller: "foo", trailing_slash: true, action: "bar", id: "33" } - assert_equal("http://www.basecamphq.com/foo/bar/33/", W.new.url_for(options) ) + assert_equal("http://www.basecamphq.com/foo/bar/33/", W.new.url_for(options)) end def test_trailing_slash_with_protocol add_host! - options = { trailing_slash: true,protocol: "https", controller: "foo", action: "bar", id: "33" } - assert_equal("https://www.basecamphq.com/foo/bar/33/", W.new.url_for(options) ) + options = { trailing_slash: true, protocol: "https", controller: "foo", action: "bar", id: "33" } + assert_equal("https://www.basecamphq.com/foo/bar/33/", W.new.url_for(options)) assert_equal "https://www.basecamphq.com/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string")) end @@ -238,7 +238,7 @@ module AbstractController assert_equal "/foo/", W.new.url_for(options.merge(only_path: true)) options.update(action: "bar", id: "33") assert_equal "/foo/bar/33/", W.new.url_for(options.merge(only_path: true)) - assert_equal "/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string",only_path: true)) + assert_equal "/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string", only_path: true)) end def test_trailing_slash_with_anchor @@ -386,7 +386,7 @@ module AbstractController def test_url_action_controller_parameters add_host! - assert_raise(ArgumentError) do + assert_raise(ActionController::UnfilteredParameters) do W.new.url_for(ActionController::Parameters.new(controller: "c", action: "a", protocol: "javascript", f: "%0Aeval(name)")) end end @@ -423,7 +423,7 @@ module AbstractController first_class.default_url_options[:host] = first_host second_class.default_url_options[:host] = second_host - assert_equal first_host, first_class.default_url_options[:host] + assert_equal first_host, first_class.default_url_options[:host] assert_equal second_host, second_class.default_url_options[:host] end @@ -487,6 +487,27 @@ module AbstractController end end + def test_default_params_first_empty + with_routing do |set| + set.draw do + get "(:param1)/test(/:param2)" => "index#index", + defaults: { + param1: 1, + param2: 2 + }, + constraints: { + param1: /\d*/, + param2: /\d+/ + } + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = "www.basecamphq.com" + + assert_equal "http://www.basecamphq.com/test", kls.new.url_for(controller: "index", param1: "1") + end + end + private def extract_params(url) url.split("?", 2).last.split("&").sort diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb index 57e21a22c6..29a5dfc0ad 100644 --- a/actionpack/test/dispatch/callbacks_test.rb +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -35,24 +35,6 @@ class DispatcherTest < ActiveSupport::TestCase assert_equal 6, Foo.b end - def test_to_prepare_and_cleanup_delegation - prepared = cleaned = false - assert_deprecated do - ActionDispatch::Callbacks.to_prepare { prepared = true } - ActionDispatch::Callbacks.to_prepare { cleaned = true } - end - - assert_deprecated do - ActionDispatch::Reloader.prepare! - end - assert prepared - - assert_deprecated do - ActionDispatch::Reloader.cleanup! - end - assert cleaned - end - private def dispatch(&block) diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 6dcd62572a..e5646de82e 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -43,7 +43,7 @@ class CookieJarTest < ActiveSupport::TestCase def test_each request.cookie_jar["foo"] = :bar list = [] - request.cookie_jar.each do |k,v| + request.cookie_jar.each do |k, v| list << [k, v] end @@ -52,7 +52,7 @@ class CookieJarTest < ActiveSupport::TestCase def test_enumerable request.cookie_jar["foo"] = :bar - actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] } + actual = request.cookie_jar.map { |k, v| [k.to_s, v.to_s] } assert_equal [["foo", "bar"]], actual end @@ -95,17 +95,17 @@ class CookiesTest < ActionController::TestCase end def authenticate_for_fourteen_days - cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) } + cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10, 5) } head :ok end def authenticate_for_fourteen_days_with_symbols - cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10,5) } + cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10, 5) } head :ok end def set_multiple_cookies - cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) } + cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10, 5) } cookies["login"] = "XJ-122" head :ok end @@ -205,7 +205,7 @@ class CookiesTest < ActionController::TestCase def delete_and_set_cookie cookies.delete :user_name - cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10,5) } + cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10, 5) } head :ok end @@ -272,6 +272,10 @@ class CookiesTest < ActionController::TestCase def noop head :ok end + + def encrypted_cookie + cookies.encrypted["foo"] + end end tests TestController @@ -284,8 +288,7 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2) @request.env["action_dispatch.signed_cookie_salt"] = - @request.env["action_dispatch.encrypted_cookie_salt"] = - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT @request.host = "www.nextangle.com" end @@ -527,9 +530,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raise TypeError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -538,9 +539,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raises TypeError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -549,9 +548,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raises ::JSON::ParserError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -560,9 +557,7 @@ class CookiesTest < ActionController::TestCase get :set_wrapped_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "wrapped: bar", cookies[:foo] - assert_raises ::JSON::ParserError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "wrapped: bar", cookies.encrypted[:foo] end @@ -573,38 +568,16 @@ class CookiesTest < ActionController::TestCase assert_equal "bar was dumped and loaded", cookies.encrypted[:foo] end - def test_encrypted_cookie_using_custom_digest - @request.env["action_dispatch.cookies_digest"] = "SHA256" - get :set_encrypted_cookie - cookies = @controller.send :cookies - assert_not_equal "bar", cookies[:foo] - assert_equal "bar", cookies.encrypted[:foo] - - sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - - sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: "SHA1") - sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: "SHA256") - - assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do - sha1_verifier.verify(cookies[:foo]) - end - - assert_nothing_raised do - sha256_verifier.verify(cookies[:foo]) - end - end - def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json @request.env["action_dispatch.cookies_serializer"] = :hybrid - key_generator = @request.env["action_dispatch.key_generator"] - encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] - encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) - sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) - marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") - @request.headers["Cookie"] = "foo=#{marshal_value}" + marshal_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}" get :get_encrypted_cookie @@ -612,40 +585,28 @@ class CookiesTest < ActionController::TestCase assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + assert_not_nil @response.cookies["foo"] + assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value @request.env["action_dispatch.cookies_serializer"] = :hybrid - key_generator = @request.env["action_dispatch.key_generator"] - encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] - encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) - sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) - json_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") - @request.headers["Cookie"] = "foo=#{json_value}" - - get :get_encrypted_cookie - - cookies = @controller.send :cookies - assert_not_equal "bar", cookies[:foo] - assert_equal "bar", cookies.encrypted[:foo] - - assert_nil @response.cookies["foo"] - end + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) - def test_compat_encrypted_cookie_using_64_byte_key - # Cookie generated with 64 bytes secret - message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") - @request.headers["Cookie"] = "foo=#{message}" + json_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}" get :get_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] + assert_nil @response.cookies["foo"] end @@ -809,10 +770,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -838,8 +799,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :json @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -848,10 +807,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -877,8 +836,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -887,10 +844,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -916,8 +873,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") @@ -926,10 +881,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -940,8 +895,8 @@ class CookiesTest < ActionController::TestCase @request.headers["Cookie"] = "user_id=45" get :get_signed_cookie - assert_equal nil, @controller.send(:cookies).signed[:user_id] - assert_equal nil, @response.cookies["user_id"] + assert_nil @controller.send(:cookies).signed[:user_id] + assert_nil @response.cookies["user_id"] end def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered @@ -951,8 +906,91 @@ class CookiesTest < ActionController::TestCase @request.headers["Cookie"] = "foo=baz" get :get_encrypted_cookie - assert_equal nil, @controller.send(:cookies).encrypted[:foo] - assert_equal nil, @response.cookies["foo"] + assert_nil @controller.send(:cookies).encrypted[:foo] + assert_nil @response.cookies["foo"] + end + + def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_cipher = "aes-256-gcm" + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_cipher = "aes-256-gcm" + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + # Cookie generated with 64 bytes secret + message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") + @request.headers["Cookie"] = "foo=#{message}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + cipher = "aes-256-gcm" + + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) + + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_cookie_with_all_domain_option @@ -1189,6 +1227,12 @@ class CookiesTest < ActionController::TestCase assert_equal "david", cookies[:user_name] end + def test_cookies_are_not_cleared + cookies.encrypted["foo"] = "bar" + get :noop + assert_equal "bar", @controller.encrypted_cookie + end + private def assert_cookie_header(expected) header = @response.headers["Set-Cookie"] diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 2c5e09e283..ea477e8908 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -287,7 +287,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "does not show filtered parameters" do @app = DevelopmentApp - get "/", params: { "foo"=>"bar" }, headers: { "action_dispatch.show_exceptions" => true, + get "/", params: { "foo" => "bar" }, headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.parameter_filter" => [:foo] } assert_response 500 assert_match(""foo"=>"[FILTERED]"", body) @@ -384,6 +384,23 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end + test "logs with non active support loggers" do + @app = DevelopmentApp + io = StringIO.new + logger = Logger.new(io) + + _old, ActionView::Base.logger = ActionView::Base.logger, logger + begin + assert_nothing_raised do + get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger } + end + ensure + ActionView::Base.logger = _old + end + + assert_match(/puke/, io.rewind && io.read) + end + test "uses backtrace cleaner from env" do @app = DevelopmentApp backtrace_cleaner = ActiveSupport::BacktraceCleaner.new diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index 6febd5cb68..958450072e 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -155,8 +155,8 @@ class HeaderTest < ActiveSupport::TestCase headers = make_headers(env) headers["Referer"] = "http://example.com/" headers.merge! "CONTENT_TYPE" => "text/plain" - assert_equal({ "HTTP_REFERER"=>"http://example.com/", - "CONTENT_TYPE"=>"text/plain" }, env) + assert_equal({ "HTTP_REFERER" => "http://example.com/", + "CONTENT_TYPE" => "text/plain" }, env) end test "fetch exception" do diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 27da5935b5..481aa22b10 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -18,14 +18,6 @@ class MiddlewareStackTest < ActiveSupport::TestCase @stack.use BarMiddleware end - def test_delete_with_string_is_deprecated - assert_deprecated do - assert_difference "@stack.size", -1 do - @stack.delete FooMiddleware.name - end - end - end - def test_delete_works assert_difference "@stack.size", -1 do @stack.delete FooMiddleware @@ -39,24 +31,6 @@ class MiddlewareStackTest < ActiveSupport::TestCase assert_equal BazMiddleware, @stack.last.klass end - test "use should push middleware as a string onto the stack" do - assert_deprecated do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" - end - assert_equal BazMiddleware, @stack.last.klass - end - end - - test "use should push middleware as a symbol onto the stack" do - assert_deprecated do - assert_difference "@stack.size" do - @stack.use :"MiddlewareStackTest::BazMiddleware" - end - assert_equal BazMiddleware, @stack.last.klass - end - end - test "use should push middleware class with arguments onto the stack" do assert_difference "@stack.size" do @stack.use BazMiddleware, true, foo: "bar" @@ -107,10 +81,8 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "unshift adds a new middleware at the beginning of the stack" do - assert_deprecated do - @stack.unshift :"MiddlewareStackTest::BazMiddleware" - assert_equal BazMiddleware, @stack.first.klass - end + @stack.unshift MiddlewareStackTest::BazMiddleware + assert_equal BazMiddleware, @stack.first.klass end test "raise an error on invalid index" do @@ -123,15 +95,6 @@ class MiddlewareStackTest < ActiveSupport::TestCase end end - test "lazy evaluates middleware class" do - assert_deprecated do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" - end - assert_equal BazMiddleware, @stack.last.klass - end - end - test "can check if Middleware are equal - Class" do assert_equal @stack.last, BarMiddleware end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 421ae6e133..2ca03c535a 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -89,7 +89,7 @@ class MimeTypeTest < ActiveSupport::TestCase # (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1) test "parse other broken acceptlines" do accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*" - expect = ["image/gif", "image/x-xbitmap", "image/jpeg","image/pjpeg", "application/x-shockwave-flash", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/msword", "pronto/1.00.00", "sslvpn/1.00.00.00", "*/*"] + expect = ["image/gif", "image/x-xbitmap", "image/jpeg", "image/pjpeg", "application/x-shockwave-flash", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/msword", "pronto/1.00.00", "sslvpn/1.00.00.00", "*/*"] assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) end @@ -167,18 +167,6 @@ class MimeTypeTest < ActiveSupport::TestCase end end - test "deprecated lookup" do - assert_deprecated do - Mime::HTML - end - end - - test "deprecated const_defined?" do - assert_deprecated do - Mime.const_defined? :HTML - end - end - test "references gives preference to symbols before strings" do assert_equal :html, Mime[:html].ref another = Mime::Type.lookup("foo/bar") diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index bb2fc53add..0e093d2188 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -22,8 +22,6 @@ module TestGenerationPrefix end class WithMountedEngine < ActionDispatch::IntegrationTest - include Rack::Test::Methods - class BlogEngine < Rails::Engine routes.draw do get "/posts/:id", to: "inside_engine_generating#show", as: :post @@ -153,114 +151,114 @@ module TestGenerationPrefix # Inside Engine test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do get "/pure-awesomeness/blog/posts/1" - assert_equal "/pure-awesomeness/blog/posts/1", last_response.body + assert_equal "/pure-awesomeness/blog/posts/1", response.body end test "[ENGINE] generating application's url never uses SCRIPT_NAME from request" do get "/pure-awesomeness/blog/url_to_application" - assert_equal "/generate", last_response.body + assert_equal "/generate", response.body end test "[ENGINE] generating engine's url with polymorphic path" do get "/pure-awesomeness/blog/polymorphic_path_for_engine" - assert_equal "/pure-awesomeness/blog/posts/1", last_response.body + assert_equal "/pure-awesomeness/blog/posts/1", response.body end test "[ENGINE] url_helpers from engine have higher priority than application's url_helpers" do get "/awesome/blog/conflicting_url" - assert_equal "engine", last_response.body + assert_equal "engine", response.body end test "[ENGINE] relative path root uses SCRIPT_NAME from request" do get "/awesome/blog/relative_path_root" - verify_redirect "http://example.org/awesome/blog" + verify_redirect "http://www.example.com/awesome/blog" end test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_path_redirect" - verify_redirect "http://example.org/awesome/blog/foo" + verify_redirect "http://www.example.com/awesome/blog/foo" end test "[ENGINE] relative option root uses SCRIPT_NAME from request" do get "/awesome/blog/relative_option_root" - verify_redirect "http://example.org/awesome/blog" + verify_redirect "http://www.example.com/awesome/blog" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_option_redirect" - verify_redirect "http://example.org/awesome/blog/foo" + verify_redirect "http://www.example.com/awesome/blog/foo" end test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do get "/awesome/blog/relative_custom_root" - verify_redirect "http://example.org/awesome/blog" + verify_redirect "http://www.example.com/awesome/blog" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_custom_redirect" - verify_redirect "http://example.org/awesome/blog/foo" + verify_redirect "http://www.example.com/awesome/blog/foo" end test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_path_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_path_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_option_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_option_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_custom_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_custom_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end # Inside Application test "[APP] generating engine's route includes prefix" do get "/generate" - assert_equal "/awesome/blog/posts/1", last_response.body + assert_equal "/awesome/blog/posts/1", response.body end test "[APP] generating engine's route includes default_url_options[:script_name]" do RailsApplication.routes.default_url_options = { script_name: "/something" } get "/generate" - assert_equal "/something/awesome/blog/posts/1", last_response.body + assert_equal "/something/awesome/blog/posts/1", response.body end test "[APP] generating engine's url with polymorphic path" do get "/polymorphic_path_for_engine" - assert_equal "/awesome/blog/posts/1", last_response.body + assert_equal "/awesome/blog/posts/1", response.body end test "polymorphic_path_for_app" do get "/polymorphic_path_for_app" - assert_equal "/posts/1", last_response.body + assert_equal "/posts/1", response.body end test "[APP] generating engine's url with url_for(@post)" do get "/polymorphic_with_url_for" - assert_equal "http://example.org/awesome/blog/posts/1", last_response.body + assert_equal "http://www.example.com/awesome/blog/posts/1", response.body end test "[APP] instance variable with same name as engine" do get "/ivar_usage" - assert_equal "/awesome/blog/posts/1", last_response.body + assert_equal "/awesome/blog/posts/1", response.body end # Inside any Object @@ -322,9 +320,9 @@ module TestGenerationPrefix private def verify_redirect(url, status = 301) - assert_equal status, last_response.status - assert_equal url, last_response.headers["Location"] - assert_equal expected_redirect_body(url), last_response.body + assert_equal status, response.status + assert_equal url, response.headers["Location"] + assert_equal expected_redirect_body(url), response.body end def expected_redirect_body(url) @@ -333,8 +331,6 @@ module TestGenerationPrefix end class EngineMountedAtRoot < ActionDispatch::IntegrationTest - include Rack::Test::Methods - class BlogEngine def self.routes @routes ||= begin @@ -388,74 +384,74 @@ module TestGenerationPrefix test "generating path inside engine" do get "/posts/1" - assert_equal "/posts/1", last_response.body + assert_equal "/posts/1", response.body end test "[ENGINE] relative path root uses SCRIPT_NAME from request" do get "/relative_path_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/relative_path_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] relative option root uses SCRIPT_NAME from request" do get "/relative_option_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/relative_option_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do get "/relative_custom_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/relative_custom_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do get "/absolute_path_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/absolute_path_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do get "/absolute_option_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/absolute_option_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do get "/absolute_custom_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/absolute_custom_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end private def verify_redirect(url, status = 301) - assert_equal status, last_response.status - assert_equal url, last_response.headers["Location"] - assert_equal expected_redirect_body(url), last_response.body + assert_equal status, response.status + assert_equal url, response.headers["Location"] + assert_equal expected_redirect_body(url), response.body end def expected_redirect_body(url) diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index e74b8e40fd..9eb78fe059 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -1,32 +1,11 @@ require "abstract_unit" class ReloaderTest < ActiveSupport::TestCase - Reloader = ActionDispatch::Reloader - teardown do ActiveSupport::Reloader.reset_callbacks :prepare ActiveSupport::Reloader.reset_callbacks :complete end - def test_prepare_callbacks - a = b = c = nil - assert_deprecated do - Reloader.to_prepare { |*args| a = b = c = 1 } - Reloader.to_prepare { |*args| b = c = 2 } - Reloader.to_prepare { |*args| c = 3 } - end - - # Ensure to_prepare callbacks are not run when defined - assert_nil a || b || c - - # Run callbacks - call_and_return_body - - assert_equal 1, a - assert_equal 2, b - assert_equal 3, c - end - class MyBody < Array def initialize(&block) @on_close = block @@ -45,6 +24,23 @@ class ReloaderTest < ActiveSupport::TestCase end end + def test_prepare_callbacks + a = b = c = nil + reloader.to_prepare { |*args| a = b = c = 1 } + reloader.to_prepare { |*args| b = c = 2 } + reloader.to_prepare { |*args| c = 3 } + + # Ensure to_prepare callbacks are not run when defined + assert_nil a || b || c + + # Run callbacks + call_and_return_body + + assert_equal 1, a + assert_equal 2, b + assert_equal 3, c + end + def test_returned_body_object_always_responds_to_close body = call_and_return_body assert_respond_to body, :close @@ -62,15 +58,12 @@ class ReloaderTest < ActiveSupport::TestCase def test_condition_specifies_when_to_reload i, j = 0, 0, 0, 0 - assert_deprecated do - Reloader.to_prepare { |*args| i += 1 } - Reloader.to_cleanup { |*args| j += 1 } - end - x = Class.new(ActiveSupport::Reloader) - x.check = lambda { i < 3 } + reloader = reloader(lambda { i < 3 }) + reloader.to_prepare { |*args| i += 1 } + reloader.to_complete { |*args| j += 1 } - app = Reloader.new(lambda { |env| [200, {}, []] }, x) + app = middleware(lambda { |env| [200, {}, []] }, reloader) 5.times do resp = app.call({}) resp[2].close @@ -115,24 +108,20 @@ class ReloaderTest < ActiveSupport::TestCase assert_respond_to body, :bar end - def test_cleanup_callbacks_are_called_when_body_is_closed - cleaned = false - assert_deprecated do - Reloader.to_cleanup { cleaned = true } - end + def test_complete_callbacks_are_called_when_body_is_closed + completed = false + reloader.to_complete { completed = true } body = call_and_return_body - assert !cleaned + assert !completed body.close - assert cleaned + assert completed end def test_prepare_callbacks_arent_called_when_body_is_closed prepared = false - assert_deprecated do - Reloader.to_prepare { prepared = true } - end + reloader.to_prepare { prepared = true } body = call_and_return_body prepared = false @@ -141,45 +130,9 @@ class ReloaderTest < ActiveSupport::TestCase assert !prepared end - def test_manual_reloading - prepared = cleaned = false - assert_deprecated do - Reloader.to_prepare { prepared = true } - Reloader.to_cleanup { cleaned = true } - end - - assert_deprecated do - Reloader.prepare! - end - assert prepared - assert !cleaned - - prepared = cleaned = false - assert_deprecated do - Reloader.cleanup! - end - assert prepared - assert cleaned - end - - def test_prepend_prepare_callback - i = 10 - assert_deprecated do - Reloader.to_prepare { i += 1 } - Reloader.to_prepare(prepend: true) { i = 0 } - end - - assert_deprecated do - Reloader.prepare! - end - assert_equal 1, i - end - - def test_cleanup_callbacks_are_called_on_exceptions - cleaned = false - assert_deprecated do - Reloader.to_cleanup { cleaned = true } - end + def test_complete_callbacks_are_called_on_exceptions + completed = false + reloader.to_complete { completed = true } begin call_and_return_body do @@ -188,16 +141,25 @@ class ReloaderTest < ActiveSupport::TestCase rescue end - assert cleaned + assert completed end private def call_and_return_body(&block) - x = Class.new(ActiveSupport::Reloader) - x.check = lambda { true } + app = middleware(block || proc { [200, {}, "response"] }) + _, _, body = app.call("rack.input" => StringIO.new("")) + body + end + + def middleware(inner_app, reloader = reloader()) + ActionDispatch::Reloader.new(inner_app, reloader) + end - @response ||= "response" - @reloader ||= Reloader.new(block || proc { [200, {}, @response] }, x) - @reloader.call("rack.input" => StringIO.new(""))[2] + def reloader(check = lambda { true }) + @reloader ||= begin + reloader = Class.new(ActiveSupport::Reloader) + reloader.check = check + reloader + end end end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index d0cd32a242..10234a4815 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -75,7 +75,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest begin $stderr = StringIO.new # suppress the log json = "[\"person]\": {\"name\": \"David\"}}" - exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", json, "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => false } + exception = assert_raise(ActionDispatch::Http::Parameters::ParseError) do + post "/parse", params: json, headers: { "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => false } + end assert_equal JSON::ParserError, exception.cause.class assert_equal exception.cause.message, exception.message ensure diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index e572c722a0..e7e8c82974 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -21,7 +21,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end end - FIXTURE_PATH = File.dirname(__FILE__) + "/../../fixtures/multipart" + FIXTURE_PATH = File.expand_path("../../fixtures/multipart", __dir__) def teardown TestController.last_request_parameters = nil @@ -129,7 +129,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest params = parse_multipart("none") assert_equal %w(submit-name), params.keys.sort assert_equal "Larry", params["submit-name"] - assert_equal nil, params["files"] + assert_nil params["files"] end test "parses empty upload file" do @@ -142,7 +142,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest test "uploads and reads binary file" do with_test_routing do - fixture = FIXTURE_PATH + "/mona_lisa.jpg" + fixture = FIXTURE_PATH + "/ruby_on_rails.jpg" params = { uploaded_data: fixture_file_upload(fixture, "image/jpg") } post "/read", params: params end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index 5c992be216..2499c33cef 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -97,7 +97,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({ "action" => { "foo" => { "bar" => nil } } }, "action[foo][bar]") assert_parses({ "action" => { "foo" => { "bar" => [] } } }, "action[foo][bar][]") assert_parses({ "action" => { "foo" => [] } }, "action[foo][]") - assert_parses({ "action"=>{ "foo"=>[{ "bar"=>nil }] } }, "action[foo][][bar]") + assert_parses({ "action" => { "foo" => [{ "bar" => nil }] } }, "action[foo][][bar]") end def test_array_parses_without_nil @@ -114,7 +114,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({ "action" => { "foo" => { "bar" => [nil] } } }, "action[foo][bar][]") assert_parses({ "action" => { "foo" => [nil] } }, "action[foo][]") assert_parses({ "action" => { "foo" => [{ "bar" => nil }] } }, "action[foo][][bar]") - assert_parses({ "action" => ["1",nil] }, "action[]=1&action[]") + assert_parses({ "action" => ["1", nil] }, "action[]=1&action[]") ensure ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge end diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 311b80ea0a..228135c547 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -54,6 +54,11 @@ module ActionDispatch assert_equal %w[rails adequate], s.keys end + def test_keys_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_key], s.keys + end + def test_values s = Session.create(store, req, {}) s["rails"] = "ftw" @@ -61,6 +66,11 @@ module ActionDispatch assert_equal %w[ftw awesome], s.values end + def test_values_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_value], s.values + end + def test_clear s = Session.create(store, req, {}) s["rails"] = "ftw" @@ -113,6 +123,14 @@ module ActionDispatch def delete_session(env, id, options); 123; end }.new end + + def store_with_data + Class.new { + def load_session(env); [1, { "sample_key" => "sample_value" }]; end + def session_exists?(env); true; end + def delete_session(env, id, options); 123; end + }.new + end end class SessionIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 5c7558e48d..6721a388c1 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -55,7 +55,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest "products[second]=Pc", "=Save" ].join("&") - expected = { + expected = { "customers" => { "boston" => { "first" => { @@ -107,7 +107,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest query = [ "customers[boston][first][name]=David", "something_else=blah", - "logo=#{File.expand_path(__FILE__)}" + "logo=#{__FILE__}" ].join("&") expected = { "customers" => { @@ -118,7 +118,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest } }, "something_else" => "blah", - "logo" => File.expand_path(__FILE__), + "logo" => __FILE__, } assert_parses expected, query end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 13a87b8976..28cbde028d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -18,7 +18,7 @@ class BaseRequestTest < ActiveSupport::TestCase ActionDispatch::Http::URL.url_for(options) end - protected + private def stub_request(env = {}) ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true @trusted_proxies ||= nil @@ -94,13 +94,13 @@ class RequestIP < BaseRequestTest assert_equal "3.4.5.6", request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,192.168.0.1" - assert_equal nil, request.remote_ip + assert_nil request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "9.9.9.9, 3.4.5.6, 172.31.4.4, 10.0.0.1" assert_equal "3.4.5.6", request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address" - assert_equal nil, request.remote_ip + assert_nil request.remote_ip end test "remote ip spoof detection" do @@ -110,8 +110,8 @@ class RequestIP < BaseRequestTest request.remote_ip } assert_match(/IP spoofing attack/, e.message) - assert_match(/HTTP_X_FORWARDED_FOR="1.1.1.1"/, e.message) - assert_match(/HTTP_CLIENT_IP="2.2.2.2"/, e.message) + assert_match(/HTTP_X_FORWARDED_FOR="1\.1\.1\.1"/, e.message) + assert_match(/HTTP_CLIENT_IP="2\.2\.2\.2"/, e.message) end test "remote ip with spoof detection disabled" do @@ -154,7 +154,7 @@ class RequestIP < BaseRequestTest assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,::1" - assert_equal nil, request.remote_ip + assert_nil request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::, fc01::, fdff" assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip @@ -163,7 +163,7 @@ class RequestIP < BaseRequestTest assert_equal "FE00::", request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address" - assert_equal nil, request.remote_ip + assert_nil request.remote_ip end test "remote ip v6 spoof detection" do @@ -200,7 +200,7 @@ class RequestIP < BaseRequestTest assert_equal "3.4.5.6", request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "67.205.106.73,unknown" - assert_equal nil, request.remote_ip + assert_nil request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "9.9.9.9, 3.4.5.6, 10.0.0.1, 67.205.106.73" assert_equal "3.4.5.6", request.remote_ip @@ -222,7 +222,7 @@ class RequestIP < BaseRequestTest assert_equal "::1", request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329" - assert_equal nil, request.remote_ip + assert_nil request.remote_ip request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334" assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip @@ -345,7 +345,7 @@ class RequestPort < BaseRequestTest test "optional port" do request = stub_request "HTTP_HOST" => "www.example.org:80" - assert_equal nil, request.optional_port + assert_nil request.optional_port request = stub_request "HTTP_HOST" => "www.example.org:8080" assert_equal 8080, request.optional_port @@ -537,7 +537,7 @@ class RequestCGI < BaseRequestTest assert_equal "Basic", request.auth_type assert_equal 0, request.content_length - assert_equal nil, request.content_mime_type + assert_nil request.content_mime_type assert_equal "CGI/1.1", request.gateway_interface assert_equal "*/*", request.accept assert_equal "UTF-8", request.accept_charset @@ -581,7 +581,7 @@ class RequestCookie < BaseRequestTest # some Nokia phone browsers omit the space after the semicolon separator. # some developers have grown accustomed to using comma in cookie values. - request = stub_request("HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes") + request = stub_request("HTTP_COOKIE" => "_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes") assert_equal "c84ace847", request.cookies["_session_id"], request.cookies.inspect assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect end @@ -596,7 +596,7 @@ class RequestParamsParsing < BaseRequestTest "rack.input" => StringIO.new("flamenco=love") ) - assert_equal({ "flamenco"=> "love" }, request.request_parameters) + assert_equal({ "flamenco" => "love" }, request.request_parameters) end test "doesnt interpret request uri as query string when missing" do @@ -760,8 +760,8 @@ class RequestMethod < BaseRequestTest end test "post uneffected by local inflections" do - existing_acrnoyms = ActiveSupport::Inflector.inflections.acronyms.dup - existing_acrnoym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup + existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup + existing_acronym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup begin ActiveSupport::Inflector.inflections do |inflect| inflect.acronym "POS" @@ -774,8 +774,8 @@ class RequestMethod < BaseRequestTest ensure # Reset original acronym set ActiveSupport::Inflector.inflections do |inflect| - inflect.send(:instance_variable_set,"@acronyms",existing_acrnoyms) - inflect.send(:instance_variable_set,"@acronym_regex",existing_acrnoym_regex) + inflect.send(:instance_variable_set, "@acronyms", existing_acronyms) + inflect.send(:instance_variable_set, "@acronym_regex", existing_acronym_regex) end end end @@ -957,7 +957,7 @@ class RequestMimeType < BaseRequestTest end test "no content type" do - assert_equal nil, stub_request.content_mime_type + assert_nil stub_request.content_mime_type end test "content type is XML" do @@ -978,7 +978,7 @@ class RequestMimeType < BaseRequestTest "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" ) - assert_equal nil, request.negotiate_mime([Mime[:xml], Mime[:json]]) + assert_nil request.negotiate_mime([Mime[:xml], Mime[:json]]) assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime[:html]]) assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime::ALL]) end @@ -1072,14 +1072,14 @@ end class RequestParameterFilter < BaseRequestTest test "process parameter filter" do test_hashes = [ - [{ "foo"=>"bar" },{ "foo"=>"bar" },%w'food'], - [{ "foo"=>"bar" },{ "foo"=>"[FILTERED]" },%w'foo'], - [{ "foo"=>"bar", "bar"=>"foo" },{ "foo"=>"[FILTERED]", "bar"=>"foo" },%w'foo baz'], - [{ "foo"=>"bar", "baz"=>"foo" },{ "foo"=>"[FILTERED]", "baz"=>"[FILTERED]" },%w'foo baz'], - [{ "bar"=>{ "foo"=>"bar","bar"=>"foo" } },{ "bar"=>{ "foo"=>"[FILTERED]","bar"=>"foo" } },%w'fo'], - [{ "foo"=>{ "foo"=>"bar","bar"=>"foo" } },{ "foo"=>"[FILTERED]" },%w'f banana'], - [{ "deep"=>{ "cc"=>{ "code"=>"bar","bar"=>"foo" },"ss"=>{ "code"=>"bar" } } },{ "deep"=>{ "cc"=>{ "code"=>"[FILTERED]","bar"=>"foo" },"ss"=>{ "code"=>"bar" } } },%w'deep.cc.code'], - [{ "baz"=>[{ "foo"=>"baz" }, "1"] }, { "baz"=>[{ "foo"=>"[FILTERED]" }, "1"] }, [/foo/]]] + [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], + [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'], + [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'], + [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'], + [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'], + [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'], + [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], + [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]] test_hashes.each do |before_filter, after_filter, filter_words| parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) @@ -1091,8 +1091,8 @@ class RequestParameterFilter < BaseRequestTest } parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - before_filter["barg"] = { :bargain=>"gain", "blah"=>"bar", "bar"=>{ "bargain"=>{ "blah"=>"foo" } } } - after_filter["barg"] = { :bargain=>"niag", "blah"=>"[FILTERED]", "bar"=>{ "bargain"=>{ "blah"=>"[FILTERED]" } } } + before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo" } } } + after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]" } } } assert_equal after_filter, parameter_filter.filter(before_filter) end @@ -1192,7 +1192,7 @@ class RequestEtag < BaseRequestTest test "doesn't match absent If-None-Match" do request = stub_request - assert_equal nil, request.if_none_match + assert_nil request.if_none_match assert_equal [], request.if_none_match_etags assert_not request.etag_matches?("foo") diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 4e547ab7d5..7433c5ce0c 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -74,7 +74,7 @@ class ResponseTest < ActiveSupport::TestCase @response.body = "Hello, World!" # even though there's no explicitly set content-type, - assert_equal nil, @response.content_type + assert_nil @response.content_type # after the action reads back @response.body, assert_equal "Hello, World!", @response.body @@ -110,6 +110,11 @@ class ResponseTest < ActiveSupport::TestCase assert_equal "application/aaron", @response.content_type end + def test_empty_content_type_returns_nil + @response.headers["Content-Type"] = "" + assert_nil @response.content_type + end + test "simple output" do @response.body = "Hello, World!" @@ -131,7 +136,7 @@ class ResponseTest < ActiveSupport::TestCase def test_only_set_charset_still_defaults_to_text_html response = ActionDispatch::Response.new response.charset = "utf-16" - _,headers,_ = response.to_a + _, headers, _ = response.to_a assert_equal "text/html; charset=utf-16", headers["Content-Type"] end @@ -229,7 +234,7 @@ class ResponseTest < ActiveSupport::TestCase test "multiple cookies" do @response.set_cookie("user_name", value: "david", path: "/") - @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10,5)) + @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5)) _status, headers, _body = @response.to_a assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"] assert_equal({ "login" => "foo&bar", "user_name" => "david" }, @response.cookies) @@ -237,7 +242,7 @@ class ResponseTest < ActiveSupport::TestCase test "delete cookies" do @response.set_cookie("user_name", value: "david", path: "/") - @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10,5)) + @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5)) @response.delete_cookie("login") assert_equal({ "user_name" => "david", "login" => nil }, @response.cookies) end diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb new file mode 100644 index 0000000000..338992dda5 --- /dev/null +++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb @@ -0,0 +1,325 @@ +require "abstract_unit" + +class TestCustomUrlHelpers < ActionDispatch::IntegrationTest + class Linkable + attr_reader :id + + def self.name + super.demodulize + end + + def initialize(id) + @id = id + end + + def linkable_type + self.class.name.underscore + end + end + + class Category < Linkable; end + class Collection < Linkable; end + class Product < Linkable; end + class Manufacturer < Linkable; end + + class Model + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + + def initialize(id = nil) + @id = id + end + + remove_method :model_name + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, self.class.name.demodulize) + end + + def persisted? + false + end + end + + class Basket < Model; end + class User < Model; end + class Video < Model; end + + class Article + attr_reader :id + + def self.name + "Article" + end + + def initialize(id) + @id = id + end + end + + class Page + attr_reader :id + + def self.name + super.demodulize + end + + def initialize(id) + @id = id + end + end + + class CategoryPage < Page; end + class ProductPage < Page; end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + default_url_options host: "www.example.com" + + root to: "pages#index" + get "/basket", to: "basket#show", as: :basket + get "/posts/:id", to: "posts#show", as: :post + get "/profile", to: "users#profile", as: :profile + get "/media/:id", to: "media#show", as: :media + get "/pages/:id", to: "pages#show", as: :page + + resources :categories, :collections, :products, :manufacturers + + namespace :admin do + get "/dashboard", to: "dashboard#index" + end + + direct(:website) { "http://www.rubyonrails.org" } + direct("string") { "http://www.rubyonrails.org" } + direct(:helper) { basket_url } + direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] } + direct(:nested) { |linkable| route_for(:linkable, linkable) } + direct(:params) { |params| params } + direct(:symbol) { :basket } + direct(:hash) { { controller: "basket", action: "show" } } + direct(:array) { [:admin, :dashboard] } + direct(:options) { |options| [:products, options] } + direct(:defaults, size: 10) { |options| [:products, options] } + + direct(:browse, page: 1, size: 10) do |options| + [:products, options.merge(params.permit(:page, :size).to_h.symbolize_keys)] + end + + resolve("Article") { |article| [:post, { id: article.id }] } + resolve("Basket") { |basket| [:basket] } + resolve("Manufacturer") { |manufacturer| route_for(:linkable, manufacturer) } + resolve("User", anchor: "details") { |user, options| [:profile, options] } + resolve("Video") { |video| [:media, { id: video.id }] } + resolve(%w[Page CategoryPage ProductPage]) { |page| [:page, { id: page.id }] } + end + + APP = build_app Routes + + def app + APP + end + + include Routes.url_helpers + + def setup + @category = Category.new("1") + @collection = Collection.new("2") + @product = Product.new("3") + @manufacturer = Manufacturer.new("apple") + @basket = Basket.new + @user = User.new + @video = Video.new("4") + @article = Article.new("5") + @page = Page.new("6") + @category_page = CategoryPage.new("7") + @product_page = ProductPage.new("8") + @path_params = { "controller" => "pages", "action" => "index" } + @unsafe_params = ActionController::Parameters.new(@path_params) + @safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action) + end + + def params + ActionController::Parameters.new(page: 2, size: 25) + end + + def test_direct_paths + assert_equal "/", website_path + assert_equal "/", Routes.url_helpers.website_path + + assert_equal "/", string_path + assert_equal "/", Routes.url_helpers.string_path + + assert_equal "/basket", helper_path + assert_equal "/basket", Routes.url_helpers.helper_path + + assert_equal "/categories/1", linkable_path(@category) + assert_equal "/categories/1", Routes.url_helpers.linkable_path(@category) + assert_equal "/collections/2", linkable_path(@collection) + assert_equal "/collections/2", Routes.url_helpers.linkable_path(@collection) + assert_equal "/products/3", linkable_path(@product) + assert_equal "/products/3", Routes.url_helpers.linkable_path(@product) + + assert_equal "/categories/1", nested_path(@category) + assert_equal "/categories/1", Routes.url_helpers.nested_path(@category) + + assert_equal "/", params_path(@safe_params) + assert_equal "/", Routes.url_helpers.params_path(@safe_params) + assert_raises(ActionController::UnfilteredParameters) { params_path(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { Routes.url_helpers.params_path(@unsafe_params) } + + assert_equal "/basket", symbol_path + assert_equal "/basket", Routes.url_helpers.symbol_path + assert_equal "/basket", hash_path + assert_equal "/basket", Routes.url_helpers.hash_path + assert_equal "/admin/dashboard", array_path + assert_equal "/admin/dashboard", Routes.url_helpers.array_path + + assert_equal "/products?page=2", options_path(page: 2) + assert_equal "/products?page=2", Routes.url_helpers.options_path(page: 2) + assert_equal "/products?size=10", defaults_path + assert_equal "/products?size=10", Routes.url_helpers.defaults_path + assert_equal "/products?size=20", defaults_path(size: 20) + assert_equal "/products?size=20", Routes.url_helpers.defaults_path(size: 20) + + assert_equal "/products?page=2&size=25", browse_path + assert_raises(NameError) { Routes.url_helpers.browse_path } + end + + def test_direct_urls + assert_equal "http://www.rubyonrails.org", website_url + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_url + + assert_equal "http://www.rubyonrails.org", string_url + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.string_url + + assert_equal "http://www.example.com/basket", helper_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.helper_url + + assert_equal "http://www.example.com/categories/1", linkable_url(@category) + assert_equal "http://www.example.com/categories/1", Routes.url_helpers.linkable_url(@category) + assert_equal "http://www.example.com/collections/2", linkable_url(@collection) + assert_equal "http://www.example.com/collections/2", Routes.url_helpers.linkable_url(@collection) + assert_equal "http://www.example.com/products/3", linkable_url(@product) + assert_equal "http://www.example.com/products/3", Routes.url_helpers.linkable_url(@product) + + assert_equal "http://www.example.com/categories/1", nested_url(@category) + assert_equal "http://www.example.com/categories/1", Routes.url_helpers.nested_url(@category) + + assert_equal "http://www.example.com/", params_url(@safe_params) + assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params) + assert_raises(ActionController::UnfilteredParameters) { params_url(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { Routes.url_helpers.params_url(@unsafe_params) } + + assert_equal "http://www.example.com/basket", symbol_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url + assert_equal "http://www.example.com/basket", hash_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.hash_url + assert_equal "http://www.example.com/admin/dashboard", array_url + assert_equal "http://www.example.com/admin/dashboard", Routes.url_helpers.array_url + + assert_equal "http://www.example.com/products?page=2", options_url(page: 2) + assert_equal "http://www.example.com/products?page=2", Routes.url_helpers.options_url(page: 2) + assert_equal "http://www.example.com/products?size=10", defaults_url + assert_equal "http://www.example.com/products?size=10", Routes.url_helpers.defaults_url + assert_equal "http://www.example.com/products?size=20", defaults_url(size: 20) + assert_equal "http://www.example.com/products?size=20", Routes.url_helpers.defaults_url(size: 20) + + assert_equal "http://www.example.com/products?page=2&size=25", browse_url + assert_raises(NameError) { Routes.url_helpers.browse_url } + end + + def test_resolve_paths + assert_equal "/basket", polymorphic_path(@basket) + assert_equal "/basket", Routes.url_helpers.polymorphic_path(@basket) + + assert_equal "/profile#details", polymorphic_path(@user) + assert_equal "/profile#details", Routes.url_helpers.polymorphic_path(@user) + + assert_equal "/profile#password", polymorphic_path(@user, anchor: "password") + assert_equal "/profile#password", Routes.url_helpers.polymorphic_path(@user, anchor: "password") + + assert_equal "/media/4", polymorphic_path(@video) + assert_equal "/media/4", Routes.url_helpers.polymorphic_path(@video) + assert_equal "/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @video) + + assert_equal "/posts/5", polymorphic_path(@article) + assert_equal "/posts/5", Routes.url_helpers.polymorphic_path(@article) + assert_equal "/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @article) + + assert_equal "/pages/6", polymorphic_path(@page) + assert_equal "/pages/6", Routes.url_helpers.polymorphic_path(@page) + assert_equal "/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @page) + + assert_equal "/pages/7", polymorphic_path(@category_page) + assert_equal "/pages/7", Routes.url_helpers.polymorphic_path(@category_page) + assert_equal "/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @category_page) + + assert_equal "/pages/8", polymorphic_path(@product_page) + assert_equal "/pages/8", Routes.url_helpers.polymorphic_path(@product_page) + assert_equal "/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @product_page) + + assert_equal "/manufacturers/apple", polymorphic_path(@manufacturer) + assert_equal "/manufacturers/apple", Routes.url_helpers.polymorphic_path(@manufacturer) + end + + def test_resolve_urls + assert_equal "http://www.example.com/basket", polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket) + + assert_equal "http://www.example.com/profile#details", polymorphic_url(@user) + assert_equal "http://www.example.com/profile#details", Routes.url_helpers.polymorphic_url(@user) + + assert_equal "http://www.example.com/profile#password", polymorphic_url(@user, anchor: "password") + assert_equal "http://www.example.com/profile#password", Routes.url_helpers.polymorphic_url(@user, anchor: "password") + + assert_equal "http://www.example.com/media/4", polymorphic_url(@video) + assert_equal "http://www.example.com/media/4", Routes.url_helpers.polymorphic_url(@video) + assert_equal "http://www.example.com/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @video) + + assert_equal "http://www.example.com/posts/5", polymorphic_url(@article) + assert_equal "http://www.example.com/posts/5", Routes.url_helpers.polymorphic_url(@article) + assert_equal "http://www.example.com/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @article) + + assert_equal "http://www.example.com/pages/6", polymorphic_url(@page) + assert_equal "http://www.example.com/pages/6", Routes.url_helpers.polymorphic_url(@page) + assert_equal "http://www.example.com/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @page) + + assert_equal "http://www.example.com/pages/7", polymorphic_url(@category_page) + assert_equal "http://www.example.com/pages/7", Routes.url_helpers.polymorphic_url(@category_page) + assert_equal "http://www.example.com/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @category_page) + + assert_equal "http://www.example.com/pages/8", polymorphic_url(@product_page) + assert_equal "http://www.example.com/pages/8", Routes.url_helpers.polymorphic_url(@product_page) + assert_equal "http://www.example.com/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @product_page) + + assert_equal "http://www.example.com/manufacturers/apple", polymorphic_url(@manufacturer) + assert_equal "http://www.example.com/manufacturers/apple", Routes.url_helpers.polymorphic_url(@manufacturer) + end + + def test_defining_direct_inside_a_scope_raises_runtime_error + routes = ActionDispatch::Routing::RouteSet.new + + assert_raises RuntimeError do + routes.draw do + namespace :admin do + direct(:rubyonrails) { "http://www.rubyonrails.org" } + end + end + end + end + + def test_defining_resolve_inside_a_scope_raises_runtime_error + routes = ActionDispatch::Routing::RouteSet.new + + assert_raises RuntimeError do + routes.draw do + namespace :admin do + resolve("User") { "/profile" } + end + end + end + end +end diff --git a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb index 4987ed84e4..179aee9ba7 100644 --- a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb +++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb @@ -7,7 +7,7 @@ class IPv6IntegrationTest < ActionDispatch::IntegrationTest class ::BadRouteRequestController < ActionController::Base include Routes.url_helpers def index - render text: foo_path + render plain: foo_path end def foo diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 6ba52e37b6..32cd78e492 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -364,18 +364,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_pagemarks - tc = self draw do scope "pagemark", controller: "pagemarks", as: :pagemark do - tc.assert_deprecated do - get "new", path: "build" - end + get "build", action: "new", as: "new" post "create", as: "" put "update" get "remove", action: :destroy, as: :remove - tc.assert_deprecated do - get action: :show, as: :show - end + get "", action: :show, as: :show end end @@ -1608,7 +1603,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "account/login", to: redirect("/login") end - previous_host, self.host = self.host, "www.example.com:3000" + previous_host, self.host = host, "www.example.com:3000" get "/account/login" verify_redirect "http://www.example.com:3000/login" @@ -1938,7 +1933,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest post :preview, on: :new end - resource :admin, path_names: { new: "novo" }, path: "administrador" do + resource :admin, path_names: { new: "novo" }, path: "administrador" do post :preview, on: :new end @@ -3638,7 +3633,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end params = ActionController::Parameters.new(id: "1") - assert_raises ArgumentError do + assert_raises ActionController::UnfilteredParameters do root_path(params) end end @@ -3711,6 +3706,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal "/bar", bar_root_path end + def test_nested_routes_under_format_resource + draw do + resources :formats do + resources :items + end + end + + get "/formats/1/items.json" + assert_equal 200, @response.status + assert_equal "items#index", @response.body + assert_equal "/formats/1/items.json", format_items_path(1, :json) + + get "/formats/1/items/2.json" + assert_equal 200, @response.status + assert_equal "items#show", @response.body + assert_equal "/formats/1/items/2.json", format_item_path(1, 2, :json) + end + private def draw(&block) @@ -3741,7 +3754,7 @@ private https!(old_https) end - def verify_redirect(url, status=301) + def verify_redirect(url, status = 301) assert_equal status, @response.status assert_equal url, @response.headers["Location"] assert_equal expected_redirect_body(url), @response.body @@ -4168,7 +4181,7 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest end private - def verify_redirect(url, status=301) + def verify_redirect(url, status = 301) assert_equal status, @response.status assert_equal url, @response.headers["Location"] assert_equal expected_redirect_body(url), @response.body @@ -4194,7 +4207,7 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest test "parameters are reset between constraint checks" do get "/bar" - assert_equal nil, @request.params[:foo] + assert_nil @request.params[:foo] assert_equal "bar", @request.params[:bar] end end @@ -4406,7 +4419,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest end end - test "invalid UTF-8 encoding returns a 400 Bad Request" do + test "invalid UTF-8 encoding is treated as ASCII 8BIT encode" do with_routing do |set| set.draw do get "/bar/:id", to: redirect("/foo/show/%{id}") @@ -4422,19 +4435,19 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest end get "/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :not_found get "/foo/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :not_found get "/foo/show/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :ok get "/bar/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :redirect get "/foobar/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :ok end end end @@ -4684,29 +4697,32 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest include Routes.url_helpers - test "url helpers raise a helpful error message when generation fails" do + test "url helpers raise a 'missing keys' error for a nil param with optimized helpers" do url, missing = { action: "show", controller: "products", id: nil }, [:id] - message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + message = "No route matches #{url.inspect}, missing required keys: #{missing.inspect}" - # Optimized url helper error = assert_raises(ActionController::UrlGenerationError) { product_path(nil) } assert_equal message, error.message + end + + test "url helpers raise a 'constraint failure' error for a nil param with non-optimized helpers" do + url, missing = { action: "show", controller: "products", id: nil }, [:id] + message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}" - # Non-optimized url helper error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil) } assert_equal message, error.message end - test "url helpers raise message with mixed parameters when generation fails " do - url, missing = { action: "show", controller: "products", id: nil, "id"=>"url-tested" }, [:id] - message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + test "url helpers raise message with mixed parameters when generation fails" do + url, missing = { action: "show", controller: "products", id: nil, "id" => "url-tested" }, [:id] + message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}" # Optimized url helper - error = assert_raises(ActionController::UrlGenerationError) { product_path(nil, "id"=>"url-tested") } + error = assert_raises(ActionController::UrlGenerationError) { product_path(nil, "id" => "url-tested") } assert_equal message, error.message # Non-optimized url helper - error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil, "id"=>"url-tested") } + error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil, "id" => "url-tested") } assert_equal message, error.message end end @@ -4915,3 +4931,113 @@ class TestInternalRoutingParams < ActionDispatch::IntegrationTest ) end end + +class FlashRedirectTest < ActionDispatch::IntegrationTest + SessionKey = "_myapp_session" + Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + + class KeyGeneratorMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.key_generator"] ||= Generator + @app.call(env) + end + end + + class FooController < ActionController::Base + def bar + render plain: (flash[:foo] || "foo") + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + get "/foo", to: redirect { |params, req| req.flash[:foo] = "bar"; "/bar" } + get "/bar", to: "flash_redirect_test/foo#bar" + end + + APP = build_app Routes do |middleware| + middleware.use KeyGeneratorMiddleware + middleware.use ActionDispatch::Session::CookieStore, key: SessionKey + middleware.use ActionDispatch::Flash + middleware.delete ActionDispatch::ShowExceptions + end + + def app + APP + end + + include Routes.url_helpers + + def test_block_redirect_commits_flash + get "/foo", env: { "action_dispatch.key_generator" => Generator } + assert_response :redirect + + follow_redirect! + assert_equal "bar", response.body + end +end + +class TestRecognizePath < ActionDispatch::IntegrationTest + class PageConstraint + attr_reader :key, :pattern + + def initialize(key, pattern) + @key = key + @pattern = pattern + end + + def matches?(request) + request.path_parameters[key] =~ pattern + end + end + + stub_controllers do |routes| + Routes = routes + routes.draw do + get "/hash/:foo", to: "pages#show", constraints: { foo: /foo/ } + get "/hash/:bar", to: "pages#show", constraints: { bar: /bar/ } + + get "/proc/:foo", to: "pages#show", constraints: proc { |r| r.path_parameters[:foo] =~ /foo/ } + get "/proc/:bar", to: "pages#show", constraints: proc { |r| r.path_parameters[:bar] =~ /bar/ } + + get "/class/:foo", to: "pages#show", constraints: PageConstraint.new(:foo, /foo/) + get "/class/:bar", to: "pages#show", constraints: PageConstraint.new(:bar, /bar/) + end + end + + APP = build_app Routes + def app + APP + end + + def test_hash_constraints_dont_leak_between_routes + expected_params = { controller: "pages", action: "show", bar: "bar" } + actual_params = recognize_path("/hash/bar") + + assert_equal expected_params, actual_params + end + + def test_proc_constraints_dont_leak_between_routes + expected_params = { controller: "pages", action: "show", bar: "bar" } + actual_params = recognize_path("/proc/bar") + + assert_equal expected_params, actual_params + end + + def test_class_constraints_dont_leak_between_routes + expected_params = { controller: "pages", action: "show", bar: "bar" } + actual_params = recognize_path("/class/bar") + + assert_equal expected_params, actual_params + end + + private + + def recognize_path(*args) + Routes.recognize_path(*args) + end +end diff --git a/actionpack/test/dispatch/runner_test.rb b/actionpack/test/dispatch/runner_test.rb index 969933c9ed..b76bf4a320 100644 --- a/actionpack/test/dispatch/runner_test.rb +++ b/actionpack/test/dispatch/runner_test.rb @@ -4,7 +4,6 @@ class RunnerTest < ActiveSupport::TestCase test "runner preserves the setting of integration_session" do runner = Class.new do def before_setup - end end.new diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index a60629a7ee..859059063f 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -142,20 +142,20 @@ class CacheStoreTest < ActionDispatch::IntegrationTest get "/get_session_value" assert_response :success - assert_equal nil, headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists" + assert_nil headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists" end end def test_prevents_session_fixation with_test_route_set do - assert_equal nil, @cache.read("_session_id:0xhax") + assert_nil @cache.read("_session_id:0xhax") cookies["_session_id"] = "0xhax" get "/set_session_value" assert_response :success assert_not_equal "0xhax", cookies["_session_id"] - assert_equal nil, @cache.read("_session_id:0xhax") + assert_nil @cache.read("_session_id:0xhax") assert_equal({ "foo" => "bar" }, @cache.read("_session_id:#{cookies['_session_id']}")) end end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 013d289c6d..63dfc07c0d 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -108,7 +108,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set(secure: true) do get "/set_session_value" assert_response :success - assert_equal nil, headers["Set-Cookie"] + assert_nil headers["Set-Cookie"] end end @@ -169,17 +169,17 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/no_session_access" assert_response :success - assert_equal nil, headers["Set-Cookie"] + assert_nil headers["Set-Cookie"] end end def test_doesnt_write_session_cookie_if_session_is_unchanged with_test_route_set do - cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" + + cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" \ "fef868465920f415f2c0652d6910d3af288a0367" get "/no_session_access" assert_response :success - assert_equal nil, headers["Set-Cookie"] + assert_nil headers["Set-Cookie"] end end diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index c2d0719b4e..121e9ebef7 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -157,7 +157,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest get "/get_session_value" assert_response :success - assert_equal nil, headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists" + assert_nil headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists" end rescue Dalli::RingError => ex skip ex.message, ex.backtrace diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index d8f673c212..3513534d72 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -11,7 +11,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest begin raise StandardError.new rescue - raise ActionDispatch::ParamsParser::ParseError + raise ActionDispatch::Http::Parameters::ParseError end when "/method_not_allowed" raise ActionController::MethodNotAllowed, "PUT" diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 71b274bf1e..757e26973f 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -12,25 +12,16 @@ class SSLTest < ActionDispatch::IntegrationTest end class RedirectSSLTest < SSLTest - def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil, - deprecated_port: nil) - - self.app = build_app ssl_options: { redirect: redirect, - host: deprecated_host, port: deprecated_port - } - + def assert_not_redirected(url, headers: {}, redirect: {}) + self.app = build_app ssl_options: { redirect: redirect } get url, headers: headers assert_response :ok end - def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil, - from: "http://a/b?c=d", to: from.sub("http", "https")) - + def assert_redirected(redirect: {}, from: "http://a/b?c=d", to: from.sub("http", "https")) redirect = { status: 301, body: [] }.merge(redirect) - self.app = build_app ssl_options: { redirect: redirect, - host: deprecated_host, port: deprecated_port - } + self.app = build_app ssl_options: { redirect: redirect } get from assert_response redirect[:status] || 301 @@ -99,18 +90,6 @@ class RedirectSSLTest < SSLTest assert_redirected redirect: { host: "ssl:443" }, to: "https://ssl:443/b?c=d" end - test ":host is deprecated, moved within redirect: { host: … }" do - assert_deprecated do - assert_redirected deprecated_host: "foo", to: "https://foo/b?c=d" - end - end - - test ":port is deprecated, moved within redirect: { port: … }" do - assert_deprecated do - assert_redirected deprecated_port: 1, to: "https://a:1/b?c=d" - end - end - test "no redirect with redirect set to false" do assert_not_redirected "http://example.org", redirect: false end @@ -123,7 +102,11 @@ class StrictTransportSecurityTest < SSLTest def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {}) self.app = build_app ssl_options: { hsts: hsts }, headers: headers get url - assert_equal expected, response.headers["Strict-Transport-Security"] + if expected.nil? + assert_nil response.headers["Strict-Transport-Security"] + else + assert_equal expected, response.headers["Strict-Transport-Security"] + end end test "enabled by default" do @@ -139,23 +122,19 @@ class StrictTransportSecurityTest < SSLTest end test "hsts: true enables default settings" do - assert_hsts EXPECTED, hsts: true + assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: true end test "hsts: false sets max-age to zero, clearing browser HSTS settings" do - assert_hsts "max-age=0", hsts: false + assert_hsts "max-age=0; includeSubDomains", hsts: false end test ":expires sets max-age" do - assert_deprecated do - assert_hsts "max-age=500", hsts: { expires: 500 } - end + assert_hsts "max-age=500; includeSubDomains", hsts: { expires: 500 } end test ":expires supports AS::Duration arguments" do - assert_deprecated do - assert_hsts "max-age=31557600", hsts: { expires: 1.year } - end + assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year } end test "include subdomains" do @@ -167,15 +146,11 @@ class StrictTransportSecurityTest < SSLTest end test "opt in to browser preload lists" do - assert_deprecated do - assert_hsts "#{EXPECTED}; preload", hsts: { preload: true } - end + assert_hsts "#{EXPECTED_WITH_SUBDOMAINS}; preload", hsts: { preload: true } end test "opt out of browser preload lists" do - assert_deprecated do - assert_hsts EXPECTED, hsts: { preload: false } - end + assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: { preload: false } end end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index f72823a80e..3082d1072b 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -44,16 +44,6 @@ module StaticTests assert_equal "Hello, World!", get("/doorkeeper%00").body end - def test_sets_cache_control - app = assert_deprecated do - ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") - end - response = Rack::MockRequest.new(app).request("GET", "/index.html") - - assert_html "/index.html", response - assert_equal "public, max-age=60", response.headers["Cache-Control"] - end - def test_serves_static_index_at_root assert_html "/index.html", get("/index.html") assert_html "/index.html", get("/index") @@ -153,16 +143,16 @@ module StaticTests assert_equal "Accept-Encoding", response.headers["Vary"] assert_equal "gzip", response.headers["Content-Encoding"] - response = get(file_name, "HTTP_ACCEPT_ENCODING" => "Gzip") - assert_gzip file_name, response + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "Gzip") + assert_gzip file_name, response - response = get(file_name, "HTTP_ACCEPT_ENCODING" => "GZIP") - assert_gzip file_name, response + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "GZIP") + assert_gzip file_name, response - response = get(file_name, "HTTP_ACCEPT_ENCODING" => "compress;q=0.5, gzip;q=1.0") - assert_gzip file_name, response + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "compress;q=0.5, gzip;q=1.0") + assert_gzip file_name, response - response = get(file_name, "HTTP_ACCEPT_ENCODING" => "") + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "") assert_not_equal "gzip", response.headers["Content-Encoding"] end @@ -173,10 +163,10 @@ module StaticTests assert_equal file_name, env["PATH_INFO"] end - def test_serves_gzip_with_propper_content_type_fallback + def test_serves_gzip_with_proper_content_type_fallback file_name = "/gzip/foo.zoo" response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip") - assert_gzip file_name, response + assert_gzip file_name, response default_response = get(file_name) # no gzip assert_equal default_response.headers["Content-Type"], response.headers["Content-Type"] @@ -187,9 +177,9 @@ module StaticTests last_modified = File.mtime(File.join(@root, "#{file_name}.gz")) response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip", "HTTP_IF_MODIFIED_SINCE" => last_modified.httpdate) assert_equal 304, response.status - assert_equal nil, response.headers["Content-Type"] - assert_equal nil, response.headers["Content-Encoding"] - assert_equal nil, response.headers["Vary"] + assert_nil response.headers["Content-Type"] + assert_nil response.headers["Content-Encoding"] + assert_nil response.headers["Vary"] end def test_serves_files_with_headers @@ -214,7 +204,7 @@ module StaticTests end # Windows doesn't allow \ / : * ? " < > | in filenames - unless RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ + unless Gem.win_platform? def test_serves_static_file_with_colon with_static_file "/foo/foo:bar.html" do |file| assert_html file, get("/foo/foo%3Abar.html") @@ -234,7 +224,7 @@ module StaticTests def assert_gzip(file_name, response) expected = File.read("#{FIXTURE_LOAD_PATH}/#{public_path}" + file_name) - actual = Zlib::GzipReader.new(StringIO.new(response.body)).read + actual = ActiveSupport::Gzip.decompress(response.body) assert_equal expected, actual end @@ -279,14 +269,14 @@ class StaticTest < ActiveSupport::TestCase filename = "shared.html.erb" assert File.exist?(File.join(@root, "..", filename)) env = { - "REQUEST_METHOD"=>"GET", - "REQUEST_PATH"=>"/..%2F#{filename}", - "PATH_INFO"=>"/..%2F#{filename}", - "REQUEST_URI"=>"/..%2F#{filename}", - "HTTP_VERSION"=>"HTTP/1.1", - "SERVER_NAME"=>"localhost", - "SERVER_PORT"=>"8080", - "QUERY_STRING"=>"" + "REQUEST_METHOD" => "GET", + "REQUEST_PATH" => "/..%2F#{filename}", + "PATH_INFO" => "/..%2F#{filename}", + "REQUEST_URI" => "/..%2F#{filename}", + "HTTP_VERSION" => "HTTP/1.1", + "SERVER_NAME" => "localhost", + "SERVER_PORT" => "8080", + "QUERY_STRING" => "" } assert_equal(DummyApp.call(nil), @app.call(env)) end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb new file mode 100644 index 0000000000..814e1d707b --- /dev/null +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -0,0 +1,21 @@ +require "abstract_unit" +require "action_dispatch/system_testing/driver" + +class DriverTest < ActiveSupport::TestCase + test "initializing the driver" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium) + assert_equal :selenium, driver.instance_variable_get(:@name) + end + + 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 [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "selenium? returns false if driver is poltergeist" do + assert_not ActionDispatch::SystemTesting::Driver.new(:poltergeist).send(:selenium?) + end +end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb new file mode 100644 index 0000000000..a83818fd80 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -0,0 +1,41 @@ +require "abstract_unit" +require "action_dispatch/system_testing/test_helpers/screenshot_helper" +require "capybara/dsl" + +class ScreenshotHelperTest < ActiveSupport::TestCase + test "image path is saved in tmp directory" do + new_test = DrivenBySeleniumWithChrome.new("x") + + assert_equal "tmp/screenshots/x.png", new_test.send(:image_path) + end + + test "image path includes failures text if test did not pass" do + new_test = DrivenBySeleniumWithChrome.new("x") + + new_test.stub :passed?, false do + assert_equal "tmp/screenshots/failures_x.png", new_test.send(:image_path) + end + end + + test "image path does not include failures text if test skipped" do + new_test = DrivenBySeleniumWithChrome.new("x") + + new_test.stub :passed?, false do + new_test.stub :skipped?, true do + assert_equal "tmp/screenshots/x.png", new_test.send(:image_path) + end + end + end +end + +class RackTestScreenshotsTest < DrivenByRackTest + test "rack_test driver does not support screenshot" do + assert_not self.send(:supports_screenshot?) + end +end + +class SeleniumScreenshotsTest < DrivenBySeleniumWithChrome + test "selenium driver supports screenshot" do + assert self.send(:supports_screenshot?) + end +end diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb new file mode 100644 index 0000000000..10412d6367 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -0,0 +1,17 @@ +require "abstract_unit" +require "capybara/dsl" +require "action_dispatch/system_testing/server" + +class ServerTest < ActiveSupport::TestCase + setup do + ActionDispatch::SystemTesting::Server.new.run + end + + test "initializing the server port" do + assert_includes Capybara.servers, :rails_puma + end + + test "port is always included" do + assert Capybara.always_include_port, "expected Capybara.always_include_port to be true" + 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 new file mode 100644 index 0000000000..8f90e45f5f --- /dev/null +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -0,0 +1,33 @@ +require "abstract_unit" + +class SetDriverToRackTestTest < DrivenByRackTest + test "uses rack_test" do + assert_equal :rack_test, Capybara.current_driver + end +end + +class OverrideSeleniumSubclassToRackTestTest < DrivenBySeleniumWithChrome + driven_by :rack_test + + test "uses rack_test" do + assert_equal :rack_test, Capybara.current_driver + end +end + +class SetDriverToSeleniumTest < DrivenBySeleniumWithChrome + test "uses selenium" 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 + end + + test "overrides host" do + host! "http://example.com" + + assert_equal "http://example.com", Capybara.app_host + end +end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index b479af781d..85a6df4975 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -30,7 +30,7 @@ class TestRequestTest < ActiveSupport::TestCase req = ActionDispatch::TestRequest.create({}) assert_equal({}, req.cookies) - assert_equal nil, req.env["HTTP_COOKIE"] + assert_nil req.env["HTTP_COOKIE"] req.cookie_jar["user_name"] = "david" assert_cookies({ "user_name" => "david" }, req.cookie_jar) diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 60d0246a68..0074d2a314 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -13,6 +13,12 @@ module ActionDispatch assert_equal "foo", uf.original_filename end + 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 + end + def test_filename_should_be_in_utf_8 uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) assert_equal "UTF-8", uf.original_filename.encoding.to_s @@ -58,25 +64,25 @@ module ActionDispatch end def test_delegates_close_to_tempfile - tf = Class.new { def close(unlink_now=false); "thunderhorse" end } + tf = Class.new { def close(unlink_now = false); "thunderhorse" end } uf = Http::UploadedFile.new(tempfile: tf.new) assert_equal "thunderhorse", uf.close end def test_close_accepts_parameter - tf = Class.new { def close(unlink_now=false); "thunderhorse: #{unlink_now}" end } + tf = Class.new { def close(unlink_now = false); "thunderhorse: #{unlink_now}" end } uf = Http::UploadedFile.new(tempfile: tf.new) assert_equal "thunderhorse: true", uf.close(true) end def test_delegates_read_to_tempfile - tf = Class.new { def read(length=nil, buffer=nil); "thunderhorse" end } + tf = Class.new { def read(length = nil, buffer = nil); "thunderhorse" end } uf = Http::UploadedFile.new(tempfile: tf.new) assert_equal "thunderhorse", uf.read end def test_delegates_read_to_tempfile_with_params - tf = Class.new { def read(length=nil, buffer=nil); [length, buffer] end } + tf = Class.new { def read(length = nil, buffer = nil); [length, buffer] end } uf = Http::UploadedFile.new(tempfile: tf.new) assert_equal %w{ thunder horse }, uf.read(*%w{ thunder horse }) end diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb index 9b88fa1f5a..dfcd423978 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb @@ -1,3 +1,3 @@ <body> -<%= cache do %><p>ERB</p><% end %> +<%= cache("fragment") do %><p>ERB</p><% end %> </body> diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder index efdcc28e0f..6599579740 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder @@ -1,5 +1,5 @@ xml.body do - cache do + cache("fragment") do xml.p "Builder" end end diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb index e523b74ae3..abf7017ce6 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb @@ -1,3 +1,3 @@ <body> -<%= cache do %><p>PHONE</p><% end %> +<%= cache("fragment") do %><p>PHONE</p><% end %> </body> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb index fa5e6bd318..1148d83ad7 100644 --- a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb +++ b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb @@ -1,3 +1,3 @@ Hello -<%= cache do %>This bit's fragment cached<% end %> +<%= cache "fragment" do %>This bit's fragment cached<% end %> <%= 'Ciao' %> diff --git a/actionpack/test/fixtures/layouts/builder.builder b/actionpack/test/fixtures/layouts/builder.builder index 7c7d4b2dd1..c55488edd0 100644 --- a/actionpack/test/fixtures/layouts/builder.builder +++ b/actionpack/test/fixtures/layouts/builder.builder @@ -1,3 +1,3 @@ xml.wrapper do xml << yield -end
\ No newline at end of file +end diff --git a/actionpack/test/fixtures/multipart/mona_lisa.jpg b/actionpack/test/fixtures/multipart/mona_lisa.jpg Binary files differdeleted file mode 100644 index 5cf3bef3d0..0000000000 --- a/actionpack/test/fixtures/multipart/mona_lisa.jpg +++ /dev/null diff --git a/actionpack/test/fixtures/multipart/ruby_on_rails.jpg b/actionpack/test/fixtures/multipart/ruby_on_rails.jpg Binary files differnew file mode 100644 index 0000000000..ed284ea0ba --- /dev/null +++ b/actionpack/test/fixtures/multipart/ruby_on_rails.jpg diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder index 598d62e2fc..15c8a7f5cf 100644 --- a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder +++ b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder @@ -1 +1 @@ -xml.p "Hello world!"
\ No newline at end of file +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder index 598d62e2fc..15c8a7f5cf 100644 --- a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder +++ b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder @@ -1 +1 @@ -xml.p "Hello world!"
\ No newline at end of file +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder index 598d62e2fc..15c8a7f5cf 100644 --- a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder +++ b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder @@ -1 +1 @@ -xml.p "Hello world!"
\ No newline at end of file +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb index cd0be6d1b5..18fa5cd923 100644 --- a/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb +++ b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb @@ -1,6 +1,6 @@ module SessionAutoloadTest class Foo - def initialize(bar="baz") + def initialize(bar = "baz") @bar = bar end def inspect diff --git a/actionpack/test/fixtures/test/formatted_xml_erb.builder b/actionpack/test/fixtures/test/formatted_xml_erb.builder index 14fd3549fb..f98aaa34a5 100644 --- a/actionpack/test/fixtures/test/formatted_xml_erb.builder +++ b/actionpack/test/fixtures/test/formatted_xml_erb.builder @@ -1 +1 @@ -xml.test 'failed'
\ No newline at end of file +xml.test "failed" diff --git a/actionpack/test/fixtures/test/hello_xml_world.builder b/actionpack/test/fixtures/test/hello_xml_world.builder index e7081b89fe..d16bb6b5cb 100644 --- a/actionpack/test/fixtures/test/hello_xml_world.builder +++ b/actionpack/test/fixtures/test/hello_xml_world.builder @@ -8,4 +8,4 @@ xml.html do xml.p "monks" xml.p "wiseguys" end -end
\ No newline at end of file +end diff --git a/actionpack/test/journey/gtg/builder_test.rb b/actionpack/test/journey/gtg/builder_test.rb index 2b314cdd4e..aa8427b265 100644 --- a/actionpack/test/journey/gtg/builder_test.rb +++ b/actionpack/test/journey/gtg/builder_test.rb @@ -5,18 +5,18 @@ module ActionDispatch module GTG class TestBuilder < ActiveSupport::TestCase def test_following_states_multi - table = tt ["a|a"] + table = tt ["a|a"] assert_equal 1, table.move([0], "a").length end def test_following_states_multi_regexp - table = tt [":a|b"] + table = tt [":a|b"] assert_equal 1, table.move([0], "fooo").length assert_equal 2, table.move([0], "b").length end def test_multi_path - table = tt ["/:a/d", "/b/c"] + table = tt ["/:a/d", "/b/c"] [ [1, "/"], @@ -38,7 +38,7 @@ module ActionDispatch /articles/:id(.:format) } - sim = NFA::Simulator.new table + sim = NFA::Simulator.new table match = sim.match "/articles/new" assert_equal 2, match.memos.length @@ -52,7 +52,7 @@ module ActionDispatch /articles/new(.:format) } - sim = NFA::Simulator.new table + sim = NFA::Simulator.new table match = sim.match "/articles/new" assert_equal 2, match.memos.length diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb index 4c8b5032eb..889640fdd7 100644 --- a/actionpack/test/journey/gtg/transition_table_test.rb +++ b/actionpack/test/journey/gtg/transition_table_test.rb @@ -35,25 +35,25 @@ module ActionDispatch def test_simulate_gt sim = simulator_for ["/foo", "/bar"] - assert_match sim, "/foo" + assert_match_route sim, "/foo" end def test_simulate_gt_regexp sim = simulator_for [":foo"] - assert_match sim, "foo" + assert_match_route sim, "foo" end def test_simulate_gt_regexp_mix sim = simulator_for ["/get", "/:method/foo"] - assert_match sim, "/get" - assert_match sim, "/get/foo" + assert_match_route sim, "/get" + assert_match_route sim, "/get/foo" end def test_simulate_optional sim = simulator_for ["/foo(/bar)"] - assert_match sim, "/foo" - assert_match sim, "/foo/bar" - assert_no_match sim, "/foo/" + assert_match_route sim, "/foo" + assert_match_route sim, "/foo/bar" + assert_no_match_route sim, "/foo/" end def test_match_data @@ -65,11 +65,11 @@ module ActionDispatch sim = GTG::Simulator.new tt - match = sim.match "/get" - assert_equal [paths.first], match.memos + memos = sim.memos "/get" + assert_equal [paths.first], memos - match = sim.match "/get/foo" - assert_equal [paths.last], match.memos + memos = sim.memos "/get/foo" + assert_equal [paths.last], memos end def test_match_data_ambiguous @@ -86,13 +86,13 @@ module ActionDispatch builder = GTG::Builder.new ast sim = GTG::Simulator.new builder.transition_table - match = sim.match "/articles/new" - assert_equal [paths[1], paths[3]], match.memos + memos = sim.memos "/articles/new" + assert_equal [paths[1], paths[3]], memos end private def asts(paths) - parser = Journey::Parser.new + parser = Journey::Parser.new paths.map { |x| ast = parser.parse x ast.each { |n| n.memo = ast } @@ -109,6 +109,14 @@ module ActionDispatch def simulator_for(paths) GTG::Simulator.new tt(paths) end + + def assert_match_route(simulator, path) + assert simulator.memos(path), "Simulator should match #{path}." + end + + def assert_no_match_route(simulator, path) + assert_not simulator.memos(path) { nil }, "Simulator should not match #{path}." + end end end end diff --git a/actionpack/test/journey/nfa/simulator_test.rb b/actionpack/test/journey/nfa/simulator_test.rb index 183c892a53..38f99398cb 100644 --- a/actionpack/test/journey/nfa/simulator_test.rb +++ b/actionpack/test/journey/nfa/simulator_test.rb @@ -80,7 +80,7 @@ module ActionDispatch ast = Nodes::Or.new routes - nfa = Journey::NFA::Builder.new ast + nfa = Journey::NFA::Builder.new ast sim = Simulator.new nfa.transition_table md = sim.match "/articles" assert_equal [asts.first], md.memos diff --git a/actionpack/test/journey/nfa/transition_table_test.rb b/actionpack/test/journey/nfa/transition_table_test.rb index f3cf36a064..0bc6bc1cf8 100644 --- a/actionpack/test/journey/nfa/transition_table_test.rb +++ b/actionpack/test/journey/nfa/transition_table_test.rb @@ -53,10 +53,10 @@ module ActionDispatch end def test_alphabet - table = tt "a|:a" + table = tt "a|:a" assert_equal [/[^\.\/\?]+/, "a"], table.alphabet - table = tt "a|a" + table = tt "a|a" assert_equal ["a"], table.alphabet end diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb index d61a8c023a..2c74617944 100644 --- a/actionpack/test/journey/path/pattern_test.rb +++ b/actionpack/test/journey/path/pattern_test.rb @@ -20,7 +20,7 @@ module ActionDispatch "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar\Z}, "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))\Z}, }.each do |path, expected| - define_method(:"test_to_regexp_#{path}") do + define_method(:"test_to_regexp_#{Regexp.escape(path)}") do path = Pattern.build( path, { controller: /.+/ }, @@ -44,7 +44,7 @@ module ActionDispatch "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar}, "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))}, }.each do |path, expected| - define_method(:"test_to_non_anchored_regexp_#{path}") do + define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do path = Pattern.build( path, { controller: /.+/ }, @@ -67,7 +67,7 @@ module ActionDispatch "/:controller/*foo" => %w{ controller foo }, "/:controller/*foo/bar" => %w{ controller foo }, }.each do |path, expected| - define_method(:"test_names_#{path}") do + define_method(:"test_names_#{Regexp.escape(path)}") do path = Pattern.build( path, { controller: /.+/ }, diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index b6414fd101..8fd73970b8 100644 --- a/actionpack/test/journey/route_test.rb +++ b/actionpack/test/journey/route_test.rb @@ -26,7 +26,7 @@ module ActionDispatch end def test_path_requirements_override_defaults - path = Path::Pattern.build(":name", { name: /love/ }, "/", true) + path = Path::Pattern.build(":name", { name: /love/ }, "/", true) defaults = { name: "tender" } route = Route.build("name", nil, path, {}, [], defaults) assert_equal(/love/, route.requirements[:name]) @@ -96,7 +96,7 @@ module ActionDispatch path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)" generic = Route.build "name", nil, path, constraints, [], {} - knowledge = { id: 20, controller: "pages", action: "show" } + knowledge = { "id" => true, "controller" => true, "action" => true } routes = [specific, generic] diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index b77bf6628a..74277a4325 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -31,6 +31,11 @@ module ActionDispatch def test_normalize_path_uppercase assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz") end + + def test_normalize_path_maintains_string_encoding + path = "/foo%AAbar%AAbaz".b + assert_equal Encoding::ASCII_8BIT, Utils.normalize_path(path).encoding + end end end end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 2b99637f56..f223a125a3 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -6,8 +6,8 @@ module ActionDispatch attr_reader :mapper, :routes, :route_set, :router def setup - @app = Routing::RouteSet::Dispatcher.new({}) - @route_set = ActionDispatch::Routing::RouteSet.new + @app = Routing::RouteSet::Dispatcher.new({}) + @route_set = ActionDispatch::Routing::RouteSet.new @routes = @route_set.router.routes @router = @route_set.router @formatter = @route_set.formatter @@ -116,7 +116,7 @@ module ActionDispatch end def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes - app = lambda { |env| [200, {}, ["success!"]] } + app = lambda { |env| [200, {}, ["success!"]] } get "/weblog", to: app env = rack_env("SCRIPT_NAME" => "", "PATH_INFO" => "/weblog") @@ -233,7 +233,7 @@ module ActionDispatch nil, Hash[params], {}, - lambda { |k,v| parameterized << [k,v]; v }) + lambda { |k, v| parameterized << [k, v]; v }) assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort end @@ -289,15 +289,15 @@ module ActionDispatch relative_url_root: nil } redirection_parameters = { - "action"=>"show", + "action" => "show", } missing_key = "name" - missing_parameters ={ + missing_parameters = { missing_key => "task_1" } request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) - message = "No route matches #{Hash[request_parameters.sort_by { |k,v|k.to_s }].inspect} missing required keys: #{[missing_key.to_sym].inspect}" + message = "No route matches #{Hash[request_parameters.sort_by { |k, v|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}" error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate( @@ -338,7 +338,7 @@ module ActionDispatch route = @routes.first env = rails_env "PATH_INFO" => request_path - called = false + called = false router.recognize(env) do |r, params| assert_equal route, r @@ -358,7 +358,7 @@ module ActionDispatch get "/:segment/*splat", to: "foo#bar" env = rails_env "PATH_INFO" => request_path - called = false + called = false route = @routes.first router.recognize(env) do |r, params| @@ -395,7 +395,7 @@ module ActionDispatch get "/books(/:action(.:format))", controller: "books" route = @routes.first - env = rails_env "PATH_INFO" => "/books/list.rss" + env = rails_env "PATH_INFO" => "/books/list.rss" expected = { controller: "books", action: "list", format: "rss" } called = false router.recognize(env) do |r, params| @@ -427,7 +427,7 @@ module ActionDispatch get "/books(/:action(.:format))", to: "foo#bar" env = rails_env "PATH_INFO" => "/books/list.rss", - "REQUEST_METHOD" => "HEAD" + "REQUEST_METHOD" => "HEAD" 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 ca735ea022..d8db5ffad1 100644 --- a/actionpack/test/journey/routes_test.rb +++ b/actionpack/test/journey/routes_test.rb @@ -6,7 +6,7 @@ module ActionDispatch attr_reader :routes, :mapper def setup - @route_set = ActionDispatch::Routing::RouteSet.new + @route_set = ActionDispatch::Routing::RouteSet.new @routes = @route_set.router.routes @router = @route_set.router @mapper = ActionDispatch::Routing::Mapper.new @route_set diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index ce9522d12a..ff37d85ed8 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -1,12 +1,12 @@ require "active_model" -class Customer < Struct.new(:name, :id) +Customer = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion undef_method :to_json - def to_xml(options={}) + def to_xml(options = {}) if options[:builder] options[:builder].name name else @@ -14,7 +14,7 @@ class Customer < Struct.new(:name, :id) end end - def to_js(options={}) + def to_js(options = {}) "name: #{name.inspect}" end alias :to_text :to_js @@ -26,9 +26,13 @@ class Customer < Struct.new(:name, :id) def persisted? id.present? end + + def cache_key + "#{name}/#{id}" + end end -class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) +Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do extend ActiveModel::Naming include ActiveModel::Conversion extend ActiveModel::Translation diff --git a/actionview/.gitignore b/actionview/.gitignore new file mode 100644 index 0000000000..0a04b29786 --- /dev/null +++ b/actionview/.gitignore @@ -0,0 +1,2 @@ +/lib/assets/compiled +/tmp diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 8bd4e1e56c..122c42c5bd 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,98 +1,11 @@ -* Show cache hits and misses when rendering partials. +* Add `:json` type to `auto_discovery_link_tag` to support [JSON Feeds](https://jsonfeed.org/version/1) - Partials using the `cache` helper will show whether a render hit or missed - the cache: + *Mike Gunderloy* - ``` - Rendered messages/_message.html.erb in 1.2 ms [cache hit] - Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] - ``` +* Update `distance_of_time_in_words` helper to display better error messages + for bad input. - This removes the need for the old fragment cache logging: + *Jay Hayes* - ``` - Read fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/d0bdf2974e1ef6d31685c3b392ad0b74 (0.6ms) - Rendered messages/_message.html.erb in 1.2 ms [cache hit] - Write fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/3b4e249ac9d168c617e32e84b99218b5 (1.1ms) - Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] - ``` - Though that full output can be reenabled with - `config.action_controller.enable_fragment_cache_logging = true`. - - *Stan Lo* - -* Changed partial rendering with a collection to allow collections which - implement `to_a`. - - Extracting the collection option had an optimization to avoid unnecessary - queries of ActiveRecord Relations by calling `#to_ary` on the given - collection. Instances of `Enumerator` or `Enumerable` are valid - collections, but they do not implement `#to_ary`. By changing this to - `#to_a`, they will now be extracted and rendered as expected. - - *Steven Harman* - -* New syntax for tag helpers. Avoid positional parameters and support HTML5 by default. - Example usage of tag helpers before: - - ```ruby - tag(:br, nil, true) - content_tag(:div, content_tag(:p, "Hello world!"), class: "strong") - - <%= content_tag :div, class: "strong" do -%> - Hello world! - <% end -%> - ``` - - Example usage of tag helpers after: - - ```ruby - tag.br - tag.div tag.p("Hello world!"), class: "strong" - - <%= tag.div class: "strong" do %> - Hello world! - <% end %> - ``` - - *Marek Kirejczyk*, *Kasper Timm Hansen* - -* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` fields. - - As a new specification of the HTML 5 the text field type `datetime` will no longer exist - and it is recommended to use `datetime-local`. - Ref: https://html.spec.whatwg.org/multipage/forms.html#local-date-and-time-state-(type=datetime-local) - - *Herminio Torres* - -* Raw template handler (which is also the default template handler in Rails 5) now outputs - HTML-safe strings. - - In Rails 5 the default template handler was changed to the raw template handler. Because - the ERB template handler escaped strings by default this broke some applications that - expected plain JS or HTML files to be rendered unescaped. This fixes the issue caused - by changing the default handler by changing the Raw template handler to output HTML-safe - strings. - - *Eileen M. Uchitelle* - -* `select_tag`'s `include_blank` option for generation for blank option tag, now adds an empty space label, - when the value as well as content for option tag are empty, so that we conform with html specification. - Ref: https://www.w3.org/TR/html5/forms.html#the-option-element. - - Generation of option before: - - ```html - <option value=""></option> - ``` - - Generation of option after: - - ```html - <option value="" label=" "></option> - ``` - - *Vipul A M* - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index 8573eb1225..ac810e86d0 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/RUNNING_UJS_TESTS.rdoc b/actionview/RUNNING_UJS_TESTS.rdoc new file mode 100644 index 0000000000..a575624a06 --- /dev/null +++ b/actionview/RUNNING_UJS_TESTS.rdoc @@ -0,0 +1,7 @@ +== 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. + +rake ujs:server diff --git a/actionview/Rakefile b/actionview/Rakefile index 6c3fc59b0a..0fc38e8db4 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -1,9 +1,11 @@ require "rake/testtask" +require "fileutils" +require "open3" desc "Default Task" task default: :test -task :package +task package: %w( assets:compile assets:verify ) # Run the unit tests @@ -25,6 +27,32 @@ namespace :test do t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end + task :ujs do + begin + Dir.mkdir("log") + pid = spawn("bundle exec rackup test/ujs/config.ru -p 4567 -s puma > log/test.log 2>&1") + + start_time = Time.now + + loop do + break if system("lsof -i :4567 >/dev/null") + + if Time.now - start_time > 5 + puts "Timed out after 5 seconds" + exit 1 + end + end + + system("npm run lint && phantomjs ../ci/phantomjs.js http://localhost:4567/") + status = $?.to_i + ensure + Process.kill("KILL", pid) if pid + FileUtils.rm_f("log") + end + + exit status + end + namespace :integration do desc "ActiveRecord Integration Tests" Rake::TestTask.new(:active_record) do |t| @@ -46,8 +74,61 @@ namespace :test do end end +namespace :ujs do + desc "Starts the test server" + task :server do + system "bundle exec rackup test/ujs/config.ru -p 4567 -s puma" + end +end + +namespace :assets do + desc "Compile Action View assets" + task :compile do + require "blade" + require "sprockets" + require "sprockets/export" + Blade.build + end + + desc "Verify compiled Action View assets" + task :verify do + file = "lib/assets/compiled/rails-ujs.js" + pathname = Pathname.new("#{__dir__}/#{file}") + + print "[verify] #{file} exists " + if pathname.exist? + puts "[OK]" + else + $stderr.puts "[FAIL]" + fail + end + + print "[verify] #{file} is a UMD module " + if pathname.read =~ /module\.exports.*define\.amd/m + puts "[OK]" + else + $stderr.puts "[FAIL]" + fail + end + + print "[verify] #{__dir__} can be required as a module " + js = <<-JS + window = { Event: class {} } + class Element {} + require('#{__dir__}') + JS + _, stderr, status = Open3.capture3("node", "--print", js) + if status.success? + puts "[OK]" + else + $stderr.puts "[FAIL]\n#{stderr}" + fail + end + end +end + task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.join(File.expand_path("..", __dir__), "/tools/line_statistics") files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 7bfdfbe29a..41221dd04e 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -22,8 +22,8 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "builder", "~> 3.1" - s.add_dependency "erubis", "~> 2.7.0" - s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.2" + s.add_dependency "erubi", "~> 1.4" + s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.3" s.add_dependency "rails-dom-testing", "~> 2.0" s.add_development_dependency "actionpack", version diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE new file mode 100644 index 0000000000..befcbdc7b7 --- /dev/null +++ b/actionview/app/assets/javascripts/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2007-2017 Rails Core team + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md new file mode 100644 index 0000000000..f321b9f720 --- /dev/null +++ b/actionview/app/assets/javascripts/README.md @@ -0,0 +1,60 @@ +Ruby on Rails unobtrusive scripting adapter. +======================================== + +This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to: + +- force confirmation dialogs for various actions; +- make non-GET requests from hyperlinks; +- make forms or hyperlinks submit data asynchronously with Ajax; +- have submit buttons become automatically disabled on form submit to prevent double-clicking. + +These features are achieved by adding certain ["data" attributes][data] to your HTML markup. In Rails, they are added by the framework's template helpers. + +Requirements +------------ + +- HTML5 doctype (optional). + +If you don't use HTML5, adding "data" attributes to your HTML4 or XHTML pages might make them fail [W3C markup validation][validator]. However, this shouldn't create any issues for web browsers or other user agents. + +Installation using npm +------------ + +Run `npm install rails-ujs --save` to install the rails-ujs package. + +Installation using Yarn +------------ + +Run `yarn add rails-ujs` to install the rails-ujs package. + +Usage +------------ + +Require `rails-ujs` in your application.js manifest. + +```javascript +//= require rails-ujs +``` + +Usage with yarn +------------ + +When using with the Webpacker gem or your preferred JavaScript bundler, just +add the following to your main JS file and compile. + +```javascript +import Rails from 'rails-ujs'; +Rails.start() +``` + +How to run tests +------------ + +Run `bundle exec rake ujs:server` first, and then run the web tests by visiting http://localhost:4567 in your browser. + +## License +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" +[validator]: http://validator.w3.org/ +[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html diff --git a/actionview/app/assets/javascripts/rails-ujs.coffee b/actionview/app/assets/javascripts/rails-ujs.coffee new file mode 100644 index 0000000000..bd6e9bb881 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs.coffee @@ -0,0 +1,39 @@ +#= require ./rails-ujs/BANNER +#= export Rails +#= require_self +#= require_tree ./rails-ujs/utils +#= require_tree ./rails-ujs/features +#= require ./rails-ujs/start + +@Rails = + # Link elements bound by rails-ujs + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]' + + # Button elements bound by rails-ujs + buttonClickSelector: + selector: 'button[data-remote]:not([form]), button[data-confirm]:not([form])' + exclude: 'form button' + + # Select elements bound by rails-ujs + inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]' + + # Form elements bound by rails-ujs + formSubmitSelector: 'form' + + # Form input elements bound by rails-ujs + formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])' + + # Form input elements disabled during form submission + formDisableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled' + + # Form input elements re-enabled after form submission + formEnableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled' + + # Form file input elements + fileInputSelector: 'input[name][type=file]:not([disabled])' + + # Link onClick disable selector with possible reenable after remote submission + linkDisableSelector: 'a[data-disable-with], a[data-disable]' + + # Button onClick disable selector with possible reenable after remote submission + buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]' diff --git a/actionview/app/assets/javascripts/rails-ujs/BANNER.js b/actionview/app/assets/javascripts/rails-ujs/BANNER.js new file mode 100644 index 0000000000..47ecd66003 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/BANNER.js @@ -0,0 +1,5 @@ +/* +Unobtrusive JavaScript +https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts +Released under the MIT license + */ diff --git a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee new file mode 100644 index 0000000000..72b5aaa218 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee @@ -0,0 +1,26 @@ +#= require_tree ../utils + +{ fire, stopEverything } = Rails + +Rails.handleConfirm = (e) -> + stopEverything(e) unless allowAction(this) + +# For 'data-confirm' attribute: +# - Fires `confirm` event +# - Shows the confirmation dialog +# - Fires the `confirm:complete` event +# +# Returns `true` if no function stops the chain and user chose yes `false` otherwise. +# Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. +# Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function +# return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. +allowAction = (element) -> + message = element.getAttribute('data-confirm') + return true unless message + + answer = false + if fire(element, 'confirm') + try answer = confirm(message) + callback = fire(element, 'confirm:complete', [answer]) + + answer and callback diff --git a/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee new file mode 100644 index 0000000000..90aa3bdf0e --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee @@ -0,0 +1,82 @@ +#= require_tree ../utils + +{ matches, getData, setData, stopEverything, formElements } = Rails + +Rails.handleDisabledElement = (e) -> + element = this + stopEverything(e) if element.disabled + +# Unified function to enable an element (link, button and form) +Rails.enableElement = (e) -> + element = if e instanceof Event then e.target else e + if matches(element, Rails.linkDisableSelector) + enableLinkElement(element) + else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formEnableSelector) + enableFormElement(element) + else if matches(element, Rails.formSubmitSelector) + enableFormElements(element) + +# Unified function to disable an element (link, button and form) +Rails.disableElement = (e) -> + element = if e instanceof Event then e.target else e + if matches(element, Rails.linkDisableSelector) + disableLinkElement(element) + else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formDisableSelector) + disableFormElement(element) + else if matches(element, Rails.formSubmitSelector) + disableFormElements(element) + +# Replace element's html with the 'data-disable-with' after storing original html +# and prevent clicking on it +disableLinkElement = (element) -> + replacement = element.getAttribute('data-disable-with') + if replacement? + setData(element, 'ujs:enable-with', element.innerHTML) # store enabled state + element.innerHTML = replacement + element.addEventListener('click', stopEverything) # prevent further clicking + setData(element, 'ujs:disabled', true) + +# Restore element to its original state which was disabled by 'disableLinkElement' above +enableLinkElement = (element) -> + originalText = getData(element, 'ujs:enable-with') + if originalText? + element.innerHTML = originalText # set to old enabled state + setData(element, 'ujs:enable-with', null) # clean up cache + element.removeEventListener('click', stopEverything) # enable element + setData(element, 'ujs:disabled', null) + +# Disables form elements: +# - Caches element value in 'ujs:enable-with' data store +# - Replaces element text with value of 'data-disable-with' attribute +# - Sets disabled property to true +disableFormElements = (form) -> + formElements(form, Rails.formDisableSelector).forEach(disableFormElement) + +disableFormElement = (element) -> + replacement = element.getAttribute('data-disable-with') + if replacement? + if matches(element, 'button') + setData(element, 'ujs:enable-with', element.innerHTML) + element.innerHTML = replacement + else + setData(element, 'ujs:enable-with', element.value) + element.value = replacement + element.disabled = true + setData(element, 'ujs:disabled', true) + +# Re-enables disabled form elements: +# - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) +# - Sets disabled property to false +enableFormElements = (form) -> + formElements(form, Rails.formEnableSelector).forEach(enableFormElement) + +enableFormElement = (element) -> + originalText = getData(element, 'ujs:enable-with') + if originalText? + if matches(element, 'button') + element.innerHTML = originalText + else + element.value = originalText + setData(element, 'ujs:enable-with', null) # clean up cache + element.disabled = false + setData(element, 'ujs:disabled', null) diff --git a/actionview/app/assets/javascripts/rails-ujs/features/method.coffee b/actionview/app/assets/javascripts/rails-ujs/features/method.coffee new file mode 100644 index 0000000000..d04d9414dd --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/features/method.coffee @@ -0,0 +1,34 @@ +#= require_tree ../utils + +{ stopEverything } = Rails + +# Handles "data-method" on links such as: +# <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a> +Rails.handleMethod = (e) -> + link = this + method = link.getAttribute('data-method') + return unless method + + href = Rails.href(link) + csrfToken = Rails.csrfToken() + csrfParam = Rails.csrfParam() + form = document.createElement('form') + formContent = "<input name='_method' value='#{method}' type='hidden' />" + + if csrfParam? and csrfToken? and not Rails.isCrossDomain(href) + formContent += "<input name='#{csrfParam}' value='#{csrfToken}' type='hidden' />" + + # Must trigger submit by click on a button, else "submit" event handler won't work! + # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit + formContent += '<input type="submit" />' + + form.method = 'post' + form.action = href + form.target = link.target + form.innerHTML = formContent + form.style.display = 'none' + + document.body.appendChild(form) + form.querySelector('[type="submit"]').click() + + stopEverything(e) diff --git a/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee b/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee new file mode 100644 index 0000000000..852587042c --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee @@ -0,0 +1,90 @@ +#= require_tree ../utils + +{ + matches, getData, setData + fire, stopEverything + ajax, isCrossDomain + serializeElement +} = Rails + +# Checks "data-remote" if true to handle the request through a XHR request. +isRemote = (element) -> + value = element.getAttribute('data-remote') + value? and value isnt 'false' + +# Submits "remote" forms and links with ajax +Rails.handleRemote = (e) -> + element = this + + return true unless isRemote(element) + unless fire(element, 'ajax:before') + fire(element, 'ajax:stopped') + return false + + withCredentials = element.getAttribute('data-with-credentials') + dataType = element.getAttribute('data-type') or 'script' + + if matches(element, Rails.formSubmitSelector) + # memoized value from clicked submit button + button = getData(element, 'ujs:submit-button') + method = getData(element, 'ujs:submit-button-formmethod') or element.method + url = getData(element, 'ujs:submit-button-formaction') or element.getAttribute('action') or location.href + + # strip query string if it's a GET request + url = url.replace(/\?.*$/, '') if method.toUpperCase() is 'GET' + + if element.enctype is 'multipart/form-data' + data = new FormData(element) + data.append(button.name, button.value) if button? + else + data = serializeElement(element, button) + + setData(element, 'ujs:submit-button', null) + setData(element, 'ujs:submit-button-formmethod', null) + setData(element, 'ujs:submit-button-formaction', null) + else if matches(element, Rails.buttonClickSelector) or matches(element, Rails.inputChangeSelector) + method = element.getAttribute('data-method') + url = element.getAttribute('data-url') + data = serializeElement(element, element.getAttribute('data-params')) + else + method = element.getAttribute('data-method') + url = Rails.href(element) + data = element.getAttribute('data-params') + + ajax( + type: method or 'GET' + url: url + data: data + dataType: dataType + # stopping the "ajax:beforeSend" event will cancel the ajax request + beforeSend: (xhr, options) -> + if fire(element, 'ajax:beforeSend', [xhr, options]) + fire(element, 'ajax:send', [xhr]) + else + fire(element, 'ajax:stopped') + xhr.abort() + success: (args...) -> fire(element, 'ajax:success', args) + error: (args...) -> fire(element, 'ajax:error', args) + complete: (args...) -> fire(element, 'ajax:complete', args) + crossDomain: isCrossDomain(url) + withCredentials: withCredentials? and withCredentials isnt 'false' + ) + stopEverything(e) + +Rails.formSubmitButtonClick = (e) -> + button = this + form = button.form + return unless form + # Register the pressed submit button + setData(form, 'ujs:submit-button', name: button.name, value: button.value) if button.name + # Save attributes from button + setData(form, 'ujs:formnovalidate-button', button.formNoValidate) + setData(form, 'ujs:submit-button-formaction', button.getAttribute('formaction')) + setData(form, 'ujs:submit-button-formmethod', button.getAttribute('formmethod')) + +Rails.handleMetaClick = (e) -> + link = this + method = (link.getAttribute('data-method') or 'GET').toUpperCase() + data = link.getAttribute('data-params') + metaClick = e.metaKey or e.ctrlKey + e.stopImmediatePropagation() if metaClick and method is 'GET' and not data diff --git a/actionview/app/assets/javascripts/rails-ujs/start.coffee b/actionview/app/assets/javascripts/rails-ujs/start.coffee new file mode 100644 index 0000000000..55595ac96f --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/start.coffee @@ -0,0 +1,70 @@ +{ + fire, delegate + getData, $ + refreshCSRFTokens, CSRFProtection + enableElement, disableElement, handleDisabledElement + handleConfirm + handleRemote, formSubmitButtonClick, handleMetaClick + handleMethod +} = Rails + +# For backward compatibility +if jQuery? and jQuery.ajax? and not jQuery.rails + jQuery.rails = Rails + jQuery.ajaxPrefilter (options, originalOptions, xhr) -> + CSRFProtection(xhr) unless options.crossDomain + +Rails.start = -> + # Cut down on the number of issues from people inadvertently including + # rails-ujs twice by detecting and raising an error when it happens. + throw new Error('rails-ujs has already been loaded!') if window._rails_loaded + + # This event works the same as the load event, except that it fires every + # time the page is loaded. + # See https://github.com/rails/jquery-ujs/issues/357 + # See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching + window.addEventListener 'pageshow', -> + $(Rails.formEnableSelector).forEach (el) -> + enableElement(el) if getData(el, 'ujs:disabled') + $(Rails.linkDisableSelector).forEach (el) -> + enableElement(el) if getData(el, 'ujs:disabled') + + delegate document, Rails.linkDisableSelector, 'ajax:complete', enableElement + delegate document, Rails.linkDisableSelector, 'ajax:stopped', enableElement + delegate document, Rails.buttonDisableSelector, 'ajax:complete', enableElement + delegate document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement + + delegate document, Rails.linkClickSelector, 'click', handleDisabledElement + delegate document, Rails.linkClickSelector, 'click', handleConfirm + delegate document, Rails.linkClickSelector, 'click', handleMetaClick + delegate document, Rails.linkClickSelector, 'click', disableElement + delegate document, Rails.linkClickSelector, 'click', handleRemote + delegate document, Rails.linkClickSelector, 'click', handleMethod + + delegate document, Rails.buttonClickSelector, 'click', handleDisabledElement + delegate document, Rails.buttonClickSelector, 'click', handleConfirm + delegate document, Rails.buttonClickSelector, 'click', disableElement + delegate document, Rails.buttonClickSelector, 'click', handleRemote + + delegate document, Rails.inputChangeSelector, 'change', handleDisabledElement + delegate document, Rails.inputChangeSelector, 'change', handleConfirm + delegate document, Rails.inputChangeSelector, 'change', handleRemote + + delegate document, Rails.formSubmitSelector, 'submit', handleDisabledElement + delegate document, Rails.formSubmitSelector, 'submit', handleConfirm + delegate document, Rails.formSubmitSelector, 'submit', handleRemote + # Normal mode submit + # Slight timeout so that the submit button gets properly serialized + delegate document, Rails.formSubmitSelector, 'submit', (e) -> setTimeout((-> disableElement(e)), 13) + delegate document, Rails.formSubmitSelector, 'ajax:send', disableElement + delegate document, Rails.formSubmitSelector, 'ajax:complete', enableElement + + delegate document, Rails.formInputClickSelector, 'click', handleDisabledElement + delegate document, Rails.formInputClickSelector, 'click', handleConfirm + delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick + + document.addEventListener('DOMContentLoaded', refreshCSRFTokens) + window._rails_loaded = true + +if window.Rails is Rails and fire(document, 'rails:attachBindings') + Rails.start() diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee new file mode 100644 index 0000000000..a653d3af3d --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -0,0 +1,96 @@ +#= require ./csrf +#= require ./event + +{ CSRFProtection, fire } = Rails + +AcceptHeaders = + '*': '*/*' + text: 'text/plain' + html: 'text/html' + xml: 'application/xml, text/xml' + json: 'application/json, text/javascript' + script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript' + +Rails.ajax = (options) -> + options = prepareOptions(options) + xhr = createXHR options, -> + response = processResponse(xhr.response ? xhr.responseText, xhr.getResponseHeader('Content-Type')) + if xhr.status // 100 == 2 + options.success?(response, xhr.statusText, xhr) + else + options.error?(response, xhr.statusText, xhr) + options.complete?(xhr, xhr.statusText) + # Call beforeSend hook + options.beforeSend?(xhr, options) + # Send the request + if xhr.readyState is XMLHttpRequest.OPENED + xhr.send(options.data) + else + fire(document, 'ajaxStop') # to be compatible with jQuery.ajax + +prepareOptions = (options) -> + options.url = options.url or location.href + options.type = options.type.toUpperCase() + # append data to url if it's a GET request + if options.type is 'GET' and options.data + if options.url.indexOf('?') < 0 + options.url += '?' + options.data + else + options.url += '&' + options.data + # Use "*" as default dataType + options.dataType = '*' unless AcceptHeaders[options.dataType]? + options.accept = AcceptHeaders[options.dataType] + options.accept += ', */*; q=0.01' if options.dataType isnt '*' + options + +createXHR = (options, done) -> + xhr = new XMLHttpRequest() + # Open and setup xhr + xhr.open(options.type, options.url, true) + xhr.setRequestHeader('Accept', options.accept) + # Set Content-Type only when sending a string + # Sending FormData will automatically set Content-Type to multipart/form-data + if typeof options.data is 'string' + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8') + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') unless options.crossDomain + # Add X-CSRF-Token + CSRFProtection(xhr) + xhr.withCredentials = !!options.withCredentials + xhr.onreadystatechange = -> + done(xhr) if xhr.readyState is XMLHttpRequest.DONE + xhr + +processResponse = (response, type) -> + if typeof response is 'string' and typeof type is 'string' + if type.match(/\bjson\b/) + try response = JSON.parse(response) + else if type.match(/\b(?:java|ecma)script\b/) + script = document.createElement('script') + script.text = response + document.head.appendChild(script).parentNode.removeChild(script) + else if type.match(/\b(xml|html|svg)\b/) + parser = new DOMParser() + type = type.replace(/;.+/, '') # remove something like ';charset=utf-8' + try response = parser.parseFromString(response, type) + response + +# Default way to get an element's href. May be overridden at Rails.href. +Rails.href = (element) -> element.href + +# Determines if the request is a cross domain request. +Rails.isCrossDomain = (url) -> + originAnchor = document.createElement('a') + originAnchor.href = location.href + urlAnchor = document.createElement('a') + try + urlAnchor.href = url + # If URL protocol is false or is a string containing a single colon + # *and* host are false, assume it is not a cross-domain request + # (should only be the case for IE7 and IE compatibility mode). + # Otherwise, evaluate protocol and host of the URL against the origin + # protocol and host. + !(((!urlAnchor.protocol || urlAnchor.protocol == ':') && !urlAnchor.host) || + (originAnchor.protocol + '//' + originAnchor.host == urlAnchor.protocol + '//' + urlAnchor.host)) + catch e + # If there is an error parsing the URL, assume it is crossDomain. + true diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csrf.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csrf.coffee new file mode 100644 index 0000000000..4eb5ebb414 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/csrf.coffee @@ -0,0 +1,25 @@ +#= require ./dom + +{ $ } = Rails + +# Up-to-date Cross-Site Request Forgery token +csrfToken = Rails.csrfToken = -> + meta = document.querySelector('meta[name=csrf-token]') + meta and meta.content + +# URL param that must contain the CSRF token +csrfParam = Rails.csrfParam = -> + meta = document.querySelector('meta[name=csrf-param]') + meta and meta.content + +# Make sure that every Ajax request sends the CSRF token +Rails.CSRFProtection = (xhr) -> + token = csrfToken() + xhr.setRequestHeader('X-CSRF-Token', token) if token? + +# Make sure that all forms have actual up-to-date tokens (cached forms contain old ones) +Rails.refreshCSRFTokens = -> + token = csrfToken() + param = csrfParam() + if token? and param? + $('form input[name="' + param + '"]').forEach (input) -> input.value = token diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee new file mode 100644 index 0000000000..6bef618147 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee @@ -0,0 +1,28 @@ +m = Element.prototype.matches or + Element.prototype.matchesSelector or + Element.prototype.mozMatchesSelector or + Element.prototype.msMatchesSelector or + Element.prototype.oMatchesSelector or + Element.prototype.webkitMatchesSelector + +Rails.matches = (element, selector) -> + if selector.exclude? + m.call(element, selector.selector) and not m.call(element, selector.exclude) + else + m.call(element, selector) + +# get and set data on a given element using "expando properties" +# See: https://developer.mozilla.org/en-US/docs/Glossary/Expando +expando = '_ujsData' + +Rails.getData = (element, key) -> + element[expando]?[key] + +Rails.setData = (element, key, value) -> + element[expando] ?= {} + element[expando][key] = value + +# a wrapper for document.querySelectorAll +# returns an Array +Rails.$ = (selector) -> + Array.prototype.slice.call(document.querySelectorAll(selector)) diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee new file mode 100644 index 0000000000..8d3ff007ea --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee @@ -0,0 +1,40 @@ +#= require ./dom + +{ matches } = Rails + +# Polyfill for CustomEvent in IE9+ +# https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill +CustomEvent = window.CustomEvent + +if typeof CustomEvent isnt 'function' + CustomEvent = (event, params) -> + evt = document.createEvent('CustomEvent') + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) + evt + CustomEvent.prototype = window.Event.prototype + +# Triggers a custom event on an element and returns false if the event result is false +fire = Rails.fire = (obj, name, data) -> + event = new CustomEvent( + name, + bubbles: true, + cancelable: true, + detail: data, + ) + obj.dispatchEvent(event) + !event.defaultPrevented + +# Helper function, needed to provide consistent behavior in IE +Rails.stopEverything = (e) -> + fire(e.target, 'ujs:everythingStopped') + e.preventDefault() + e.stopPropagation() + e.stopImmediatePropagation() + +Rails.delegate = (element, selector, eventType, handler) -> + element.addEventListener eventType, (e) -> + target = e.target + target = target.parentNode until not (target instanceof Element) or matches(target, selector) + if target instanceof Element and handler.call(target, e) == false + e.preventDefault() + e.stopPropagation() diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee new file mode 100644 index 0000000000..5fa337b518 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee @@ -0,0 +1,36 @@ +#= require ./dom + +{ matches } = Rails + +toArray = (e) -> Array.prototype.slice.call(e) + +Rails.serializeElement = (element, additionalParam) -> + inputs = [element] + inputs = toArray(element.elements) if matches(element, 'form') + params = [] + + inputs.forEach (input) -> + return unless input.name + if matches(input, 'select') + toArray(input.options).forEach (option) -> + params.push(name: input.name, value: option.value) if option.selected + else if input.checked or ['radio', 'checkbox', 'submit'].indexOf(input.type) == -1 + params.push(name: input.name, value: input.value) + + params.push(additionalParam) if additionalParam + + params.map (param) -> + if param.name? + "#{encodeURIComponent(param.name)}=#{encodeURIComponent(param.value)}" + else + param + .join('&') + +# Helper function that returns form elements that match the specified CSS selector +# If form is actually a "form" element this will return associated elements outside the from that have +# the html form attribute set +Rails.formElements = (form, selector) -> + if matches(form, 'form') + toArray(form.elements).filter (el) -> matches(el, selector) + else + toArray(form.querySelectorAll(selector)) diff --git a/actionview/bin/test b/actionview/bin/test index 84a05bba08..a7beb14b27 100755 --- a/actionview/bin/test +++ b/actionview/bin/test @@ -2,5 +2,3 @@ COMPONENT_ROOT = File.expand_path("..", __dir__) require File.expand_path("../tools/test", COMPONENT_ROOT) - -exit Minitest.run(ARGV) diff --git a/actionview/blade.yml b/actionview/blade.yml new file mode 100644 index 0000000000..9e5eb953a4 --- /dev/null +++ b/actionview/blade.yml @@ -0,0 +1,11 @@ +load_paths: + - app/assets/javascripts + +logical_paths: + - rails-ujs.js + +build: + logical_paths: + - rails-ujs.js + path: lib/assets/compiled + clean: true diff --git a/actionview/coffeelint.json b/actionview/coffeelint.json new file mode 100644 index 0000000000..cf8bf2171b --- /dev/null +++ b/actionview/coffeelint.json @@ -0,0 +1,135 @@ +{ + "arrow_spacing": { + "level": "warn" + }, + "braces_spacing": { + "level": "warn", + "spaces": 1, + "empty_object_spaces": 0 + }, + "camel_case_classes": { + "level": "error" + }, + "coffeescript_error": { + "level": "error" + }, + "colon_assignment_spacing": { + "level": "warn", + "spacing": { + "left": 0, + "right": 1 + } + }, + "cyclomatic_complexity": { + "level": "warn", + "value": 10 + }, + "duplicate_key": { + "level": "error" + }, + "empty_constructor_needs_parens": { + "level": "warn" + }, + "ensure_comprehensions": { + "level": "warn" + }, + "eol_last": { + "level": "warn" + }, + "indentation": { + "value": 2, + "level": "error" + }, + "line_endings": { + "level": "warn", + "value": "unix" + }, + "max_line_length": { + "value": 80, + "level": "ignore", + "limitComments": true + }, + "missing_fat_arrows": { + "level": "ignore" + }, + "newlines_after_classes": { + "value": 3, + "level": "warn" + }, + "no_backticks": { + "level": "error" + }, + "no_debugger": { + "level": "warn", + "console": false + }, + "no_empty_functions": { + "level": "warn" + }, + "no_empty_param_list": { + "level": "warn" + }, + "no_implicit_braces": { + "level": "ignore", + "strict": true + }, + "no_implicit_parens": { + "level": "ignore", + "strict": true + }, + "no_interpolation_in_single_quotes": { + "level": "warn" + }, + "no_nested_string_interpolation": { + "level": "warn" + }, + "no_plusplus": { + "level": "warn" + }, + "no_private_function_fat_arrows": { + "level": "warn" + }, + "no_stand_alone_at": { + "level": "warn" + }, + "no_tabs": { + "level": "error" + }, + "no_this": { + "level": "warn" + }, + "no_throwing_strings": { + "level": "error" + }, + "no_trailing_semicolons": { + "level": "error" + }, + "no_trailing_whitespace": { + "level": "error", + "allowed_in_comments": false, + "allowed_in_empty_lines": true + }, + "no_unnecessary_double_quotes": { + "level": "warn" + }, + "no_unnecessary_fat_arrows": { + "level": "warn" + }, + "non_empty_constructor_needs_parens": { + "level": "warn" + }, + "prefer_english_operator": { + "level": "ignore", + "doubleNotLevel": "warn" + }, + "space_operators": { + "level": "warn" + }, + "spacing_after_comma": { + "level": "warn" + }, + "transform_messes_up_line_numbers": { + "level": "warn" + } +} + diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index ba6755be82..99c5b831b5 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -92,5 +92,5 @@ end require "active_support/core_ext/string/output_safety" ActiveSupport.on_load(:i18n) do - I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" + I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__) end diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index b7c05fdb88..5387174467 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -11,7 +11,7 @@ module ActionView #:nodoc: # = Action View Base # # Action View templates can be written in several ways. - # If the template file has a <tt>.erb</tt> extension, then it uses the erubis[https://rubygems.org/gems/erubis] + # If the template file has a <tt>.erb</tt> extension, then it uses the erubi[https://rubygems.org/gems/erubi] # template system which can embed Ruby into an HTML document. # If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used. # diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index ee263df484..31aa73a0cf 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -28,7 +28,7 @@ module ActionView # returns the correct buffer on +yield+. This is usually # overwritten by helpers to add more behavior. # :api: plugin - def _layout_for(name=nil) + def _layout_for(name = nil) name ||= :layout view_flow.get(name).html_safe end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 2d6ad8f6d9..5ddf1ceb66 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -6,6 +6,12 @@ module ActionView class Digestor @@digest_mutex = Mutex.new + module PerExecutionDigestCacheExpiry + def self.before(target) + ActionView::LookupContext::DetailsKey.clear + end + end + class << self # Supported options: # @@ -56,8 +62,10 @@ module ActionView node end else - logger.error " '#{name}' file doesn't exist, so no dependencies" - logger.error " Couldn't find template for digesting: #{name}" + unless name.include?("#") # Dynamic template partial names can never be tracked + logger.error " Couldn't find template for digesting: #{name}" + end + seen[name] ||= Missing.new(name, logical_name, nil) end end diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb index 16874c1194..6d5f57a570 100644 --- a/actionview/lib/action_view/flows.rb +++ b/actionview/lib/action_view/flows.rb @@ -5,7 +5,7 @@ module ActionView attr_reader :content def initialize - @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } + @content = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new } end # Called by _layout_for to read stored values. diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 5fc4f3f1b9..92e21d7a4f 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -6,7 +6,7 @@ module ActionView module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 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 b1563ac490..c21fe782c6 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -1,6 +1,5 @@ require "active_support/core_ext/array/extract_options" require "active_support/core_ext/hash/keys" -require "active_support/core_ext/regexp" require "action_view/helpers/asset_url_helper" require "action_view/helpers/tag_helper" @@ -36,18 +35,37 @@ module ActionView # When the Asset Pipeline is enabled, you can pass the name of your manifest as # source, and include other JavaScript or CoffeeScript files inside the manifest. # + # ==== Options + # + # 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 + # that path. + # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline + # when it is set to true. + # + # ==== Examples + # # javascript_include_tag "xmlhr" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.debug-1284139606.js"></script> + # + # javascript_include_tag "xmlhr", host: "localhost", protocol: "https" + # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script> # # javascript_include_tag "template.jst", extname: false - # # => <script src="/assets/template.jst?1284139606"></script> + # # => <script src="/assets/template.debug-1284139606.jst"></script> # # javascript_include_tag "xmlhr.js" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.debug-1284139606.js"></script> # # javascript_include_tag "common.javascript", "/elsewhere/cools" - # # => <script src="/assets/common.javascript?1284139606"></script> - # # <script src="/elsewhere/cools.js?1423139606"></script> + # # => <script src="/assets/common.javascript.debug-1284139606.js"></script> + # # <script src="/elsewhere/cools.debug-1284139606.js"></script> # # javascript_include_tag "http://www.example.com/xmlhr" # # => <script src="http://www.example.com/xmlhr"></script> @@ -104,9 +122,9 @@ module ActionView end # Returns a link tag that browsers and feed readers can use to auto-detect - # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or - # <tt>:atom</tt>. Control the link options in url_for format using the - # +url_options+. You can modify the LINK tag itself in +tag_options+. + # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default), + # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format + # using the +url_options+. You can modify the LINK tag itself in +tag_options+. # # ==== Options # @@ -120,6 +138,8 @@ module ActionView # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:atom) # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:json) + # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:rss, {action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"}) @@ -129,8 +149,8 @@ module ActionView # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) - if !(type == :rss || type == :atom) && tag_options[:type].blank? - raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") + if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank? + raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.") end tag( @@ -169,7 +189,7 @@ module ActionView # # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> - def favicon_link_tag(source="favicon.ico", options={}) + def favicon_link_tag(source = "favicon.ico", options = {}) tag("link", { rel: "shortcut icon", type: "image/x-icon", @@ -207,7 +227,7 @@ module ActionView # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> # image_tag("/icons/icon.gif", data: { title: 'Rails Application' }) # # => <img data-title="Rails Application" src="/icons/icon.gif" /> - def image_tag(source, options={}) + def image_tag(source, options = {}) options = options.symbolize_keys check_for_image_tag_errors(options) diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index e0de2ff4d6..03bd1eb008 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -1,5 +1,4 @@ require "zlib" -require "active_support/core_ext/regexp" module ActionView # = Action View Asset URL Helpers @@ -97,8 +96,8 @@ module ActionView # 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 - # 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. + # 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+. # # config.action_controller.asset_host = Proc.new { |source, request| # if request && request.ssl? @@ -233,12 +232,16 @@ module ActionView stylesheet: ".css" } - # Compute extname to append to asset path. Returns nil if + # Compute extname to append to asset path. Returns +nil+ if # nothing should be added. def compute_asset_extname(source, options = {}) return if options[:extname] == false extname = options[:extname] || ASSET_EXTENSIONS[options[:type]] - extname if extname && File.extname(source) != extname + if extname && File.extname(source) != extname + extname + else + nil + end end # Maps asset types to public directory. @@ -407,7 +410,7 @@ module ActionView def video_url(source, options = {}) url_to_asset(source, { type: :video }.merge!(options)) end - alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route + alias_method :url_to_video, :video_url # aliased to avoid conflicts with a video_url named route # Computes the path to an audio asset in the public audios directory. # Full paths from the document root will be passed through. @@ -446,7 +449,7 @@ module ActionView def font_path(source, options = {}) path_to_asset(source, { type: :font }.merge!(options)) end - alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + alias_method :path_to_font, :font_path # aliased to avoid conflicts with a font_path named route # Computes the full URL to a font asset. # This will use +font_path+ internally, so most of their behaviors will be the same. @@ -458,7 +461,7 @@ module ActionView def font_url(source, options = {}) url_to_asset(source, { type: :font }.merge!(options)) end - alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route + alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route end end end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index 09d243c46d..3538515aee 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -103,7 +103,7 @@ module ActionView xml = options.delete(:xml) || eval("xml", block.binding) xml.instruct! if options[:instruct] - options[:instruct].each do |target,attrs| + options[:instruct].each do |target, attrs| if attrs.respond_to?(:keys) xml.instruct!(target, attrs) elsif attrs.respond_to?(:each) @@ -113,7 +113,7 @@ module ActionView end feed_opts = { "xml:lang" => options[:language] || "en-US", "xmlns" => "http://www.w3.org/2005/Atom" } - feed_opts.merge!(options).reject! { |k,v| !k.to_s.match(/^xml/) } + feed_opts.merge!(options).reject! { |k, v| !k.to_s.match(/^xml/) } xml.feed(feed_opts) do xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}") @@ -163,7 +163,7 @@ module ActionView @xml, @view, @feed_options = xml, view, feed_options end - # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used. + # Accepts a Date or Time object and inserts it in the proper format. If +nil+ is passed, current time in UTC is used. def updated(date_or_time = nil) @xml.updated((date_or_time || Time.now.utc).xmlschema) end @@ -174,7 +174,7 @@ module ActionView # # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists. # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists. - # * <tt>:url</tt>: The URL for this entry or false or nil for not having a link tag. Defaults to the polymorphic_url for the record. + # * <tt>:url</tt>: The URL for this entry or +false+ or +nil+ for not having a link tag. Defaults to the +polymorphic_url+ for the record. # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}" # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html". def entry(record, options = {}) diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 5258a01144..c3aecadcd6 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -8,10 +8,9 @@ module ActionView # fragments, and so on. This method takes a block that contains # the content you wish to cache. # - # The best way to use this is by doing key-based cache expiration - # on top of a cache store like Memcached that'll automatically - # kick out old entries. For more on key-based expiration, see: - # http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works + # The best way to use this is by doing recyclable key-based cache expiration + # on top of a cache store like Memcached or Redis that'll automatically + # kick out old entries. # # When using this method, you list the cache dependency as the name of the cache, like so: # @@ -23,10 +22,14 @@ module ActionView # This approach will assume that when a new topic is added, you'll touch # the project. The cache key generated from this call will be something like: # - # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 - # ^class ^id ^updated_at ^template tree digest + # views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + # ^template path ^template tree digest ^class ^id # - # The cache is thus automatically bumped whenever the project updated_at is touched. + # This cache key is stable, but it's combined with a cache version derived from the project + # record. When the project updated_at is touched, the #cache_version changes, even + # if the key stays stable. This means that unlike a traditional key-based cache expiration + # approach, you won't be generating cache trash, unused keys, simply because the dependent + # record is updated. # # If your template cache depends on multiple sources (try to avoid this to keep things simple), # you can name all these dependencies as part of an array: @@ -88,7 +91,7 @@ module ActionView # # === Explicit dependencies # - # Some times you'll have template dependencies that can't be derived at all. This is typically + # Sometimes you'll have template dependencies that can't be derived at all. This is typically # the case when you have template rendering that happens in helpers. Here's an example: # # <%= render_sortable_todolists @project.todolists %> @@ -215,18 +218,23 @@ module ActionView private - def fragment_name_with_digest(name, virtual_path) #:nodoc: + def fragment_name_with_digest(name, virtual_path) virtual_path ||= @virtual_path + if virtual_path - name = controller.url_for(name).split("://").last if name.is_a?(Hash) - digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies - [ name, digest ] + name = controller.url_for(name).split("://").last if name.is_a?(Hash) + + if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence + [ "#{virtual_path}:#{digest}", name ] + else + [ virtual_path, name ] + end else name end end - def fragment_for(name = {}, options = nil, &block) #:nodoc: + def fragment_for(name = {}, options = nil, &block) if content = read_fragment_for(name, options) @cache_hit = true content @@ -236,13 +244,11 @@ module ActionView end end - def read_fragment_for(name, options) #:nodoc: + def read_fragment_for(name, options) controller.read_fragment(name, options) end - def write_fragment_for(name, options) #:nodoc: - # VIEW TODO: Make #capture usable outside of ERB - # This dance is needed because Builder can't use capture + def write_fragment_for(name, options) pos = output_buffer.length yield output_safe = output_buffer.html_safe? diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 04c5fd4218..3f43465aa4 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -95,10 +95,10 @@ module ActionView scope: :'datetime.distance_in_words' }.merge!(options) - from_time = from_time.to_time if from_time.respond_to?(:to_time) - to_time = to_time.to_time if to_time.respond_to?(:to_time) + from_time = normalize_distance_of_time_argument_to_time(from_time) + to_time = normalize_distance_of_time_argument_to_time(to_time) from_time, to_time = to_time, from_time if from_time > to_time - distance_in_minutes = ((to_time - from_time)/60.0).round + distance_in_minutes = ((to_time - from_time) / 60.0).round distance_in_seconds = (to_time - from_time).round I18n.with_options locale: options[:locale], scope: options[:scope] do |locale| @@ -130,22 +130,18 @@ module ActionView # 60 days up to 365 days when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round else - if from_time.acts_like?(:time) && to_time.acts_like?(:time) - fyear = from_time.year - fyear += 1 if from_time.month >= 3 - tyear = to_time.year - tyear -= 1 if to_time.month < 3 - leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } - minute_offset_for_leap_year = leap_years * 1440 - # Discount the leap year days when calculating year distance. - # e.g. if there are 20 leap year days between 2 dates having the same day - # and month then the based on 365 days calculation - # the distance in years will come out to over 80 years when in written - # English it would read better as about 80 years. - minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year - else - minutes_with_offset = distance_in_minutes - end + from_year = from_time.year + from_year += 1 if from_time.month >= 3 + to_year = to_time.year + to_year -= 1 if to_time.month < 3 + leap_years = (from_year > to_year) ? 0 : (from_year..to_year).count { |x| Date.leap?(x) } + minute_offset_for_leap_year = leap_years * 1440 + # Discount the leap year days when calculating year distance. + # e.g. if there are 20 leap year days between 2 dates having the same day + # and month then the based on 365 days calculation + # the distance in years will come out to over 80 years when in written + # English it would read better as about 80 years. + minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year remainder = (minutes_with_offset % MINUTES_IN_YEAR) distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR) if remainder < MINUTES_IN_QUARTER_YEAR @@ -220,7 +216,7 @@ module ActionView # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails). # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty # dates. - # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. + # * <tt>:default</tt> - Set a default date if the affected date isn't set or is +nil+. # * <tt>:selected</tt> - Set a date that overrides the actual value. # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings @@ -267,7 +263,7 @@ module ActionView # date_select("article", "written_on", default: 3.days.from_now) # # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute - # # which is set in the form with todays date, regardless of the value in the Active Record object. + # # which is set in the form with today's date, regardless of the value in the Active Record object. # date_select("article", "written_on", selected: Date.today) # # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute @@ -687,6 +683,18 @@ module ActionView content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &block) end + + private + + def normalize_distance_of_time_argument_to_time(value) + if value.is_a?(Numeric) + Time.at(value) + elsif value.respond_to?(:to_time) + value.to_time + else + raise ArgumentError, "#{value.inspect} can't be converted to a Time value" + end + end end class DateTimeSelector #:nodoc: @@ -866,7 +874,7 @@ module ActionView end # Returns translated month names, but also ensures that a custom month - # name array has a leading nil element. + # name array has a leading +nil+ element. def month_names @month_names ||= begin month_names = @options[:use_month_names] || translated_month_names diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 124a14f1d9..672269b811 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -201,9 +201,9 @@ module ActionView # <%= f.submit %> # <% end %> # - # This also works for the methods in FormOptionHelper and DateHelper that + # This also works for the methods in FormOptionsHelper and DateHelper that # are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === #form_for with a model object # @@ -416,13 +416,13 @@ module ActionView # # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter # - # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> # ... # <% end %> # # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>: # - # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> # ... # <% end %> def form_for(record, options = {}, &block) @@ -474,6 +474,268 @@ module ActionView end private :apply_form_for_options! + mattr_accessor(:form_with_generates_remote_forms) { true } + + # Creates a form tag based on mixing URLs, scopes, or models. + # + # # Using just a URL: + # <%= form_with url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="title"> + # </form> + # + # # Adding a scope prefixes the input field names: + # <%= form_with scope: :post, url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="post[title]"> + # </form> + # + # # Using a model infers both the URL and scope: + # <%= form_with model: Post.new do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="post[title]"> + # </form> + # + # # An existing model makes an update form and fills out field values: + # <%= form_with model: Post.first do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts/1" method="post" data-remote="true"> + # <input type="hidden" name="_method" value="patch"> + # <input type="text" name="post[title]" value="<the title of the post>"> + # </form> + # + # # Though the fields don't have to correspond to model attributes: + # <%= form_with model: Cat.new do |form| %> + # <%= form.text_field :cats_dont_have_gills %> + # <%= form.text_field :but_in_forms_they_can %> + # <% end %> + # # => + # <form action="/cats" method="post" data-remote="true"> + # <input type="text" name="cat[cats_dont_have_gills]"> + # <input type="text" name="cat[but_in_forms_they_can]"> + # </form> + # + # The parameters in the forms are accessible in controllers according to + # their name nesting. So inputs named +title+ and <tt>post[title]</tt> are + # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt> + # respectively. + # + # By default +form_with+ attaches the <tt>data-remote</tt> attribute + # submitting the form via an XMLHTTPRequest in the background if an + # Unobtrusive JavaScript driver, like rails-ujs, is used. See the + # <tt>:local</tt> option for more. + # + # For ease of comparison the examples above left out the submit button, + # as well as the auto generated hidden fields that enable UTF-8 support + # and adds an authenticity token needed for cross site request forgery + # protection. + # + # ==== +form_with+ options + # + # * <tt>:url</tt> - The URL the form submits to. Akin to values passed to + # +url_for+ or +link_to+. For example, you may use a named route + # directly. When a <tt>:scope</tt> is passed without a <tt>:url</tt> the + # form just submits to the current URL. + # * <tt>:method</tt> - The method to use when submitting the form, usually + # either "get" or "post". If "patch", "put", "delete", or another verb + # is used, a hidden input named <tt>_method</tt> is added to + # simulate the verb over post. + # * <tt>:format</tt> - The format of the route the form submits to. + # Useful when submitting to another resource type, like <tt>:json</tt>. + # Skipped if a <tt>:url</tt> is passed. + # * <tt>:scope</tt> - The scope to prefix input field names with and + # thereby how the submitted parameters are grouped in controllers. + # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and + # <tt>:scope</tt> by, plus fill out input field values. + # So if a +title+ attribute is set to "Ahoy!" then a +title+ input + # field's value would be "Ahoy!". + # If the model is a new record a create form is generated, if an + # existing record, however, an update form is generated. + # Pass <tt>:scope</tt> or <tt>:url</tt> to override the defaults. + # E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>. + # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. + # Override with a custom authenticity token or pass <tt>false</tt> to + # skip the authenticity token field altogether. + # Useful when submitting to an external resource like a payment gateway + # that might limit the valid fields. + # Remote forms may omit the embedded authenticity token by setting + # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>. + # 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. + # 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. + # * <tt>:builder</tt> - Override the object used to build the form. + # * <tt>:id</tt> - Optional HTML id attribute. + # * <tt>:class</tt> - Optional HTML class attribute. + # * <tt>:data</tt> - Optional HTML data attributes. + # * <tt>:html</tt> - Other optional HTML attributes for the form tag. + # + # === Examples + # + # When not passing a block, +form_with+ just generates an opening form tag. + # + # <%= form_with(model: @post, url: super_posts_path) %> + # <%= form_with(model: @post, scope: :article) %> + # <%= form_with(model: @post, format: :json) %> + # <%= form_with(model: @post, authenticity_token: false) %> # Disables the token. + # + # For namespaced routes, like +admin_post_url+: + # + # <%= form_with(model: [ :admin, @post ]) do |form| %> + # ... + # <% end %> + # + # If your resource has associations defined, for example, you want to add comments + # to the document given that the routes are set correctly: + # + # <%= form_with(model: [ @document, Comment.new ]) do |form| %> + # ... + # <% end %> + # + # 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 + # match the stand-alone FormHelper methods and methods + # from FormTagHelper: + # + # <%= form_with scope: :person do |form| %> + # <%= form.text_field :first_name %> + # <%= form.text_field :last_name %> + # + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", "1", @person.company.admin? %> + # + # <%= form.submit %> + # <% end %> + # + # Same goes for the methods in FormOptionsHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionsHelper#collection_select and DateHelper#datetime_select. + # + # === Setting the method + # + # You can force the form to use the full array of HTTP verbs by setting + # + # method: (:get|:post|:patch|:put|:delete) + # + # in the options hash. If the verb is not GET or POST, which are natively + # supported by HTML forms, the form will be set to POST and a hidden input + # called _method will carry the intended verb for the server to interpret. + # + # === Setting HTML options + # + # You can set data attributes directly in a data hash, but HTML options + # besides id and class must be wrapped in an HTML key: + # + # <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %> + # ... + # <% end %> + # + # generates + # + # <form action="/posts/123" method="post" data-behavior="autosave" name="go"> + # <input name="_method" type="hidden" value="patch" /> + # ... + # </form> + # + # === Removing hidden model id's + # + # The +form_with+ method automatically includes the model id as a hidden field in the form. + # This is used to maintain the correlation between the form data and its associated model. + # Some ORM systems do not use IDs on nested models so in this case you want to be able + # to disable the hidden id. + # + # In the following example the Post model has many Comments stored within it in a NoSQL database, + # thus there is no primary key for comments. + # + # <%= form_with(model: @post) do |form| %> + # <%= form.fields(:comments, skip_id: true) do |fields| %> + # ... + # <% end %> + # <% end %> + # + # === Customized form builders + # + # You can also build forms using a customized FormBuilder class. Subclass + # FormBuilder and override or define some more helpers, then use your + # custom builder. For example, let's say you made a helper to + # automatically add labels to form inputs. + # + # <%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %> + # <%= form.text_field :first_name %> + # <%= form.text_field :last_name %> + # <%= form.text_area :biography %> + # <%= form.check_box :admin %> + # <%= form.submit %> + # <% end %> + # + # In this case, if you use: + # + # <%= render form %> + # + # The rendered template is <tt>people/_labelling_form</tt> and the local + # variable referencing the form builder is called + # <tt>labelling_form</tt>. + # + # The custom FormBuilder class is automatically merged with the options + # of a nested +fields+ call, unless it's explicitly set. + # + # In many cases you will want to wrap the above in another helper, so you + # could do something like the following: + # + # def labelled_form_with(**options, &block) + # form_with(**options.merge(builder: LabellingFormBuilder), &block) + # 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 + + if model + url ||= polymorphic_path(model, format: format) + + model = model.last if model.is_a?(Array) + scope ||= model_name_from_record_or_class(model).param_key + end + + if block_given? + builder = instantiate_builder(scope, model, options) + output = capture(builder, &Proc.new) + options[:multipart] ||= builder.multipart? + + html_options = html_options_for_form_with(url, model, options) + form_tag_with_body(html_options, output) + else + html_options = html_options_for_form_with(url, model, options) + form_tag_html(html_options) + end + end + # Creates a scope around a specific model object like form_for, but # doesn't create the form tags themselves. This makes fields_for suitable # for specifying additional model objects in the same form. @@ -531,9 +793,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -720,6 +982,74 @@ module ActionView capture(builder, &block) end + # Scopes input fields with either an explicit scope or model. + # Like +form_with+ does with <tt>:scope</tt> or <tt>:model</tt>, + # except it doesn't output the form tags. + # + # # Using a scope prefixes the input field names: + # <%= fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # # => <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.text_field :body %> + # <% end %> + # # => + # <input type="text" name="comment[body] value="full bodied"> + # + # # Using +fields+ with +form_with+: + # <%= form_with model: @post do |form| %> + # <%= form.text_field :title %> + # + # <%= form.fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # <% end %> + # + # Much like +form_with+ a FormBuilder instance associated with the scope + # 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 + # match the stand-alone FormHelper methods and methods + # from FormTagHelper: + # + # <%= fields model: @comment do |fields| %> + # <%= fields.text_field :body %> + # + # <%= text_area :commenter, :biography %> + # <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %> + # <% end %> + # + # Same goes for the methods in FormOptionsHelper and DateHelper designed + # to work with an object as a base, like + # 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 + + if model + scope ||= model_name_from_record_or_class(model).param_key + end + + builder = instantiate_builder(scope, model, options) + capture(builder, &block) + end + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. @@ -1175,6 +1505,34 @@ module ActionView end private + def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms, + skip_enforcing_utf8: false, **options) + html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html) + html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? + html_options[:enforce_utf8] = !skip_enforcing_utf8 + + html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart) + + # The following URL is unescaped, this is just a hash of options, and it is the + # responsibility of the caller to escape all the values. + html_options[:action] = url_for(url_for_options || {}) + html_options[:"accept-charset"] = "UTF-8" + html_options[:"data-remote"] = true unless local + + html_options[:authenticity_token] = options.delete(:authenticity_token) + + if !local && html_options[:authenticity_token].blank? + html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms + end + + if html_options[:authenticity_token] == true + # Include the default authenticity_token, which is only generated when it's set to nil, + # but we needed the true value to override the default of no authenticity_token on data-remote. + html_options[:authenticity_token] = nil + end + + html_options.stringify_keys! + end def instantiate_builder(record_name, record_object, options) case record_name @@ -1183,7 +1541,7 @@ module ActionView object_name = record_name else object = record_name - object_name = model_name_from_record_or_class(object).param_key + object_name = model_name_from_record_or_class(object).param_key if object end builder = options[:builder] || default_form_builder_class @@ -1248,14 +1606,15 @@ module ActionView include ModelNaming # The methods which wrap a form helper call. - class_attribute :field_helpers - self.field_helpers = [:fields_for, :label, :text_field, :password_field, - :hidden_field, :file_field, :text_area, :check_box, - :radio_button, :color_field, :search_field, - :telephone_field, :phone_field, :date_field, - :time_field, :datetime_field, :datetime_local_field, - :month_field, :week_field, :url_field, :email_field, - :number_field, :range_field] + class_attribute :field_helpers, default: [ + :fields_for, :fields, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field + ] attr_accessor :object_name, :object, :options @@ -1285,7 +1644,10 @@ module ActionView def initialize(object_name, object, template, options) @nested_child_index = {} @object_name, @object, @template, @options = object_name, object, template, options - @default_options = @options ? @options.slice(:index, :namespace) : {} + @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {} + + convert_to_legacy_options(@options) + if @object_name.to_s.match(/\[\]$/) if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param) @auto_index = object.to_param @@ -1293,11 +1655,12 @@ module ActionView raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" end end + @multipart = nil @index = options[:index] || options[:child_index] end - (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector| + (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( @@ -1366,9 +1729,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -1566,10 +1929,11 @@ module ActionView record_name = model_name_from_record_or_class(record_object).param_key end + object_name = @object_name index = if options.has_key?(:index) options[:index] elsif defined?(@auto_index) - self.object_name = @object_name.to_s.sub(/\[\]$/,"") + object_name = object_name.to_s.sub(/\[\]$/, "") @auto_index end @@ -1586,6 +1950,16 @@ module ActionView @template.fields_for(record_name, record_object, fields_options, &block) end + # 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 + + convert_to_legacy_options(options) + + fields_for(scope || model, model, **options, &block) + end + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. @@ -1809,7 +2183,7 @@ module ActionView # post: # create: "Add %{model}" # - def submit(value=nil, options={}) + def submit(value = nil, options = {}) value, options = nil, value if value.is_a?(Hash) value ||= submit_default_value @template.submit_tag(value, options) @@ -1934,6 +2308,12 @@ module ActionView @nested_child_index[name] ||= -1 @nested_child_index[name] += 1 end + + def convert_to_legacy_options(options) + if options.key?(:skip_id) + options[:include_id] = !options.delete(:skip_id) + end + end end end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 7bd473507b..9fc08b3837 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -18,7 +18,7 @@ module ActionView include TextHelper mattr_accessor :embed_authenticity_token_in_remote_forms - self.embed_authenticity_token_in_remote_forms = false + self.embed_authenticity_token_in_remote_forms = nil # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like # ActionController::Base#url_for. The method for the form defaults to POST. @@ -442,26 +442,14 @@ module ActionView # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) - options = options.stringify_keys + options = options.deep_stringify_keys tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options) - - if ActionView::Base.automatically_disable_submit_tag - unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false) - disable_with_text = tag_options["data-disable-with"] - disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"] - disable_with_text ||= value.to_s.clone - tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) - else - tag_options["data"].delete(:disable_with) if tag_options["data"] - end - tag_options.delete("data-disable-with") - end - + set_default_disable_with value, tag_options tag :input, tag_options end # Creates a button element that defines a <tt>submit</tt> button, - # <tt>reset</tt>button or a generic button which can be used in + # <tt>reset</tt> button or a generic button which can be used in # JavaScript, for example. You can use the button tag as a regular # submit tag but it isn't supported in legacy browsers. However, # the button tag does allow for richer labels such as images and emphasis, @@ -898,6 +886,22 @@ module ActionView def sanitize_to_id(name) name.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_") end + + def set_default_disable_with(value, tag_options) + return unless ActionView::Base.automatically_disable_submit_tag + data = tag_options["data"] + + unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false) + disable_with_text = tag_options["data-disable-with"] + disable_with_text ||= data["disable_with"] if data + disable_with_text ||= value.to_s.clone + tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) + else + data.delete("disable_with") if data + end + + tag_options.delete("data-disable-with") + end end end end diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 75b898c3e9..b6bc5f4f6f 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -92,7 +92,7 @@ module ActionView # (defaults to "%u%n"). Fields are <tt>%u</tt> for the # currency, and <tt>%n</tt> for the number. # * <tt>:negative_format</tt> - Sets the format for negative - # numbers (defaults to prepending an hyphen to the formatted + # numbers (defaults to prepending a hyphen to the formatted # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. @@ -171,6 +171,9 @@ module ActionView # to ","). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). + # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for + # deriving the placement of delimiter. Helpful when using currency formats + # like INR. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # @@ -187,6 +190,9 @@ module ActionView # number_with_delimiter(98765432.98, delimiter: " ", separator: ",") # # => 98 765 432,98 # + # number_with_delimiter("123456.78", + # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) # => "1,23,456.78" + # # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) delegate_number_helper_method(:number_to_delimited, number, options) diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index 8e63e59fac..25defd1276 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -25,10 +25,10 @@ module ActionView #:nodoc: # safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />") # # => "<p>foo</p><br /><p>bar</p>" # - # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />") + # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />")) # # => "<p>foo</p><br /><p>bar</p>" # - def safe_join(array, sep=$,) + def safe_join(array, sep = $,) sep = ERB::Util.unwrapped_html_escape(sep) array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe @@ -60,7 +60,7 @@ module ActionView #:nodoc: when 2 safe_join([array[0], array[1]], options[:two_words_connector]) else - safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]]) + safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil) end end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 3d6ff598ee..0abd5bc5dc 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -13,6 +13,7 @@ module ActionView # It also strips href/src attributes with unsafe protocols like # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, # ASCII, and hex character references to work around these protocol filters. + # All special characters will be escaped. # # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information. @@ -20,8 +21,7 @@ module ActionView # Custom sanitization rules can also be provided. # # Please note that sanitizing user-provided text does not guarantee that the - # resulting markup is valid or even well-formed. For example, the output may still - # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>. + # resulting markup is valid or even well-formed. # # ==== Options # @@ -45,17 +45,15 @@ module ActionView # Providing a custom Rails::Html scrubber: # # class CommentScrubber < Rails::Html::PermitScrubber - # def allowed_node?(node) - # !%w(form script comment blockquote).include?(node.name) + # def initialize + # super + # self.tags = %w( form script comment blockquote ) + # self.attributes = %w( style ) # end # # def skip_node?(node) # node.text? # end - # - # def scrub_attribute?(name) - # name == 'style' - # end # end # # <%= sanitize @comment.body, scrubber: CommentScrubber.new %> @@ -88,7 +86,7 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from +html+, including comments. + # Strips all HTML tags from +html+, including comments and special characters. # # strip_tags("Strip <i>these</i> tags!") # # => Strip these tags! @@ -98,8 +96,11 @@ module ActionView # # strip_tags("<div id='top-bar'>Welcome to my website!</div>") # # => Welcome to my website! + # + # strip_tags("> A quote from Smith & Wesson") + # # => > A quote from Smith & Wesson def strip_tags(html) - self.class.full_sanitizer.sanitize(html, encode_special_chars: false) + self.class.full_sanitizer.sanitize(html) end # Strips all link tags from +html+ leaving just the link text. @@ -112,6 +113,9 @@ module ActionView # # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.') # # => Blog: Visit. + # + # strip_links('<<a href="https://example.org">malformed & link</a>') + # # => <malformed & link def strip_links(html) self.class.link_sanitizer.sanitize(html) end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 24c6d03cd1..306b71c85e 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -138,7 +138,7 @@ module ActionView # # ==== Options # - # Any passed options become attributes on the generated tag. + # Use symbol keyed options to add attributes to the generated tag. # # tag.section class: %w( kitties puppies ) # # => <section class="kitties puppies"></section> diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index e3e3c8b109..aa420c4b66 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -11,10 +11,19 @@ module ActionView @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @template_object = template_object - @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") + @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]") @object = retrieve_object(options.delete(:object)) + @skip_default_ids = options.delete(:skip_default_ids) + @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object) @options = options - @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil + + if Regexp.last_match + @generate_indexed_names = true + @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) + else + @generate_indexed_names = false + @auto_index = nil + end end # This is what child classes implement. @@ -25,7 +34,11 @@ module ActionView private def value(object) - object.public_send @method_name if object + if @allow_method_names_outside_object + object.public_send @method_name if object && object.respond_to?(@method_name) + else + object.public_send @method_name if object + end end def value_before_type_cast(object) @@ -81,15 +94,21 @@ module ActionView def add_default_name_and_id(options) index = name_and_id_index(options) options["name"] = options.fetch("name") { tag_name(options["multiple"], index) } - options["id"] = options.fetch("id") { tag_id(index) } - if namespace = options.delete("namespace") - options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace + + unless skip_default_ids? + options["id"] = options.fetch("id") { tag_id(index) } + if namespace = options.delete("namespace") + options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace + end end end def tag_name(multiple = false, index = nil) # a little duplication to construct less strings - if index + case + when @object_name.empty? + "#{sanitized_method_name}#{"[]" if multiple}" + when index "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" else "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" @@ -98,7 +117,10 @@ module ActionView def tag_id(index = nil) # a little duplication to construct less strings - if index + case + when @object_name.empty? + sanitized_method_name.dup + when index "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" else "#{sanitized_object_name}_#{sanitized_method_name}" @@ -110,7 +132,7 @@ module ActionView end def sanitized_method_name - @sanitized_method_name ||= @method_name.sub(/\?$/,"") + @sanitized_method_name ||= @method_name.sub(/\?$/, "") end def sanitized_value(value) @@ -127,7 +149,7 @@ module ActionView end value = options.fetch(:selected) { value(object) } - select = content_tag("select", add_options(option_tags, options, value), html_options) + select = content_tag("select", add_options(option_tags, options, value), html_options.except!("skip_default_ids", "allow_method_names_outside_object")) if html_options["multiple"] && options.fetch(:include_hidden, true) tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select @@ -152,7 +174,15 @@ module ActionView end def name_and_id_index(options) - options.key?("index") ? options.delete("index") || "" : @auto_index + if options.key?("index") + options.delete("index") || "" + elsif @generate_indexed_names + @auto_index || "" + end + end + + def skip_default_ids? + @skip_default_ids end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 2a6bf49567..7252d4f2d9 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -7,7 +7,7 @@ module ActionView include CollectionHelpers class CheckBoxBuilder < Builder # :nodoc: - def check_box(extra_html_options={}) + def check_box(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) html_options[:multiple] = true @template_object.check_box(@object_name, @method_name, html_options, @value, nil) @@ -24,7 +24,7 @@ module ActionView builder.check_box + builder.label end - def hidden_field_name #:nodoc: + def hidden_field_name "#{super}[]" end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 36575b2fd0..75d237eb35 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -17,7 +17,7 @@ module ActionView @input_html_options = input_html_options end - def label(label_html_options={}, &block) + def label(label_html_options = {}, &block) html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options) html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id] @@ -43,7 +43,7 @@ module ActionView # Generate default options for collection helpers, such as :checked and # :disabled. - def default_html_options_for_collection(item, value) #:nodoc: + def default_html_options_for_collection(item, value) html_options = @html_options.dup [:checked, :selected, :disabled, :readonly].each do |option| @@ -67,11 +67,11 @@ module ActionView html_options end - def sanitize_attribute_name(value) #:nodoc: + def sanitize_attribute_name(value) "#{sanitized_method_name}_#{sanitized_value(value)}" end - def render_collection #:nodoc: + def render_collection @collection.map do |item| value = value_for_collection(item, @value_method) text = value_for_collection(item, @text_method) @@ -82,7 +82,7 @@ module ActionView end.join.html_safe end - def render_collection_for(builder_class, &block) #:nodoc: + def render_collection_for(builder_class, &block) options = @options.stringify_keys rendered_collection = render_collection do |item, value, text, default_html_options| builder = instantiate_builder(builder_class, item, value, text, default_html_options) @@ -103,12 +103,12 @@ module ActionView end end - def hidden_field #:nodoc: + def hidden_field hidden_name = @html_options[:name] || hidden_field_name @template_object.hidden_field_tag(hidden_name, "", id: nil) end - def hidden_field_name #:nodoc: + def hidden_field_name "#{tag_name(false, @options[:index])}" end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index eed7941cd6..a5f72af9ff 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -7,7 +7,7 @@ module ActionView include CollectionHelpers class RadioButtonBuilder < Builder # :nodoc: - def radio_button(extra_html_options={}) + def radio_button(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) @template_object.radio_button(@object_name, @method_name, @value, html_options) end diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb index 006605885a..638c134deb 100644 --- a/actionview/lib/action_view/helpers/tags/date_select.rb +++ b/actionview/lib/action_view/helpers/tags/date_select.rb @@ -16,7 +16,7 @@ module ActionView class << self def select_type - @select_type ||= self.name.split("::").last.sub("Select", "").downcase + @select_type ||= name.split("::").last.sub("Select", "").downcase end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index b31d5fda66..cab15ae201 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -73,6 +73,10 @@ 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/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 667c7e945a..9ff7e54e4f 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -33,7 +33,7 @@ module ActionView # [nil, []] # { nil => [] } def grouped_choices? - !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last + !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last end end end diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index 4306c3543d..613cade7b3 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -17,7 +17,7 @@ module ActionView class << self def field_type - @field_type ||= self.name.split("::").last.sub("Field", "").downcase + @field_type ||= name.split("::").last.sub("Field", "").downcase end end diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb index 62b1df81c6..ced835eaa8 100644 --- a/actionview/lib/action_view/helpers/tags/translator.rb +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -14,6 +14,8 @@ module ActionView translated_attribute || human_attribute_name end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :object_name, :method_and_value, :scope, :model diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 0fea4df09c..bc922f9ce8 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -151,7 +151,7 @@ module ActionView # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+ - # isn't found, nil is returned. + # isn't found, +nil+ is returned. # # excerpt('This is an example', 'an', radius: 5) # # => ...s is an exam... @@ -187,7 +187,7 @@ module ActionView unless separator.empty? text.split(separator).each do |value| if value.match(regex) - regex = phrase = value + phrase = value break end end @@ -225,14 +225,7 @@ module ActionView # # pluralize(2, 'Person', locale: :de) # # => 2 Personen - def pluralize(count, singular, deprecated_plural = nil, plural: nil, locale: I18n.locale) - if deprecated_plural - ActiveSupport::Deprecation.warn("Passing plural as a positional argument " \ - "is deprecated and will be removed in Rails 5.1. Use e.g. " \ - "pluralize(1, 'person', plural: 'people') instead.") - plural ||= deprecated_plural - end - + def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale) word = if (count == 1 || count =~ /^1(\.0+)?$/) singular else diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index cbabaf5757..47ed41a129 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -1,6 +1,5 @@ require "action_view/helpers/tag_helper" require "active_support/core_ext/string/access" -require "active_support/core_ext/regexp" require "i18n/exceptions" module ActionView diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index dad0e9dac3..a6857101b9 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -2,7 +2,6 @@ require "action_view/helpers/javascript_helper" require "active_support/core_ext/array/access" require "active_support/core_ext/hash/keys" require "active_support/core_ext/string/output_safety" -require "active_support/core_ext/regexp" module ActionView # = Action View URL Helpers @@ -36,7 +35,7 @@ module ActionView when :back _back_url else - raise ArgumentError, "arguments passed to url_for can't be handled. Please require " + + raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \ "routes or provide your own implementation" end end @@ -106,10 +105,9 @@ module ActionView # driver to prompt with the question specified (in this case, the # resulting text would be <tt>question?</tt>. If the user accepts, the # link is processed normally, otherwise no action is taken. - # * <tt>:disable_with</tt> - Value of this parameter will be - # used as the value for a disabled version of the submit - # button when the form is submitted. This feature is provided - # by the unobtrusive JavaScript driver. + # * <tt>:disable_with</tt> - Value of this parameter will be used as the + # name for a disabled version of the link. This feature is provided by + # the unobtrusive JavaScript driver. # # ==== Examples # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments @@ -519,6 +517,9 @@ module ActionView # current_page?('http://www.example.com/shop/checkout') # # => true # + # current_page?('http://www.example.com/shop/checkout', check_parameters: true) + # # => false + # # current_page?('/shop/checkout') # # => true # @@ -532,7 +533,7 @@ module ActionView # # We can also pass in the symbol arguments instead of strings. # - def current_page?(options) + def current_page?(options, check_parameters: false) unless request raise "You cannot use helpers that need to determine the current " \ "page unless your view context provides a Request object " \ @@ -541,12 +542,14 @@ module ActionView return false unless request.get? || request.head? + check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters) url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY) # We ignore any extra parameters in the request_uri if the # submitted url doesn't have any either. This lets the function # work with things like ?order=asc - request_uri = url_string.index("?") ? request.fullpath : request.path + # the behaviour can be disabled with check_parameters: true + request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY) url_string.chomp!("/") if url_string.start_with?("/") && url_string != "/" @@ -564,7 +567,7 @@ module ActionView html_options = html_options.stringify_keys html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete("method".freeze) + method = html_options.delete("method".freeze) add_method_to_attributes!(html_options, method) if method @@ -587,7 +590,7 @@ module ActionView html_options["data-method".freeze] = method end - def token_tag(token=nil, form_options: {}) + def token_tag(token = nil, form_options: {}) if token != false && protect_against_forgery? token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) @@ -616,7 +619,13 @@ module ActionView # # to_form_params({ name: 'Denmark' }, 'country') # # => [{name: 'country[name]', value: 'Denmark'}] - def to_form_params(attribute, namespace = nil) # :nodoc: + def to_form_params(attribute, namespace = nil) + attribute = if attribute.respond_to?(:permitted?) + attribute.to_h + else + attribute + end + params = [] case attribute when Hash diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 344893f41a..ab8409e8d0 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -1,6 +1,5 @@ require "action_view/rendering" require "active_support/core_ext/module/remove_method" -require "active_support/core_ext/regexp" module ActionView # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in @@ -92,16 +91,16 @@ module ActionView # layout false # # In these examples, we have three implicit lookup scenarios: - # * The BankController uses the "bank" layout. - # * The ExchangeController uses the "exchange" layout. - # * The CurrencyController inherits the layout from BankController. + # * The +BankController+ uses the "bank" layout. + # * The +ExchangeController+ uses the "exchange" layout. + # * The +CurrencyController+ inherits the layout from BankController. # # However, when a layout is explicitly set, the explicitly set layout wins: - # * The InformationController uses the "information" layout, explicitly set. - # * The TellerController also uses the "information" layout, because the parent explicitly set it. - # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration. - # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. - # * The TillController does not use a layout at all. + # * The +InformationController+ uses the "information" layout, explicitly set. + # * The +TellerController+ also uses the "information" layout, because the parent explicitly set it. + # * The +EmployeeController+ uses the "employee" layout, because it set the layout to +nil+, resetting the parent configuration. + # * The +VaultController+ chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. + # * The +TillController+ does not use a layout at all. # # == Types of layouts # @@ -149,8 +148,8 @@ module ActionView # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>. # - # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists. - # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent: + # Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists. + # Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent: # # class ApplicationController < ActionController::Base # layout "application" @@ -205,9 +204,9 @@ module ActionView include ActionView::Rendering included do - class_attribute :_layout, :_layout_conditions, instance_accessor: false - self._layout = nil - self._layout_conditions = {} + class_attribute :_layout, instance_accessor: false + class_attribute :_layout_conditions, instance_accessor: false, default: {} + _write_layout_method end @@ -255,7 +254,7 @@ module ActionView # true:: raise an ArgumentError # nil:: Force default layout behavior with inheritance # - # Return value of Proc & Symbol arguments should be String, false, true or nil + # Return value of +Proc+ and +Symbol+ arguments should be +String+, +false+, +true+ or +nil+ # with the same meaning as described above. # ==== Parameters # * <tt>layout</tt> - The layout to use. @@ -320,7 +319,7 @@ module ActionView name_clause end - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def _layout(formats) if _conditional_layout? #{layout_definition} @@ -339,7 +338,7 @@ module ActionView # # ==== Returns # * <tt>String</tt> - A template name - def _implied_layout_name # :nodoc: + def _implied_layout_name controller_path end end @@ -405,11 +404,11 @@ module ActionView # # ==== Parameters # * <tt>formats</tt> - The formats accepted to this layout - # * <tt>require_layout</tt> - If set to true and layout is not found, - # an +ArgumentError+ exception is raised (defaults to false) + # * <tt>require_layout</tt> - If set to +true+ and layout is not found, + # an +ArgumentError+ exception is raised (defaults to +false+) # # ==== Returns - # * <tt>template</tt> - The template object for the default layout (or nil) + # * <tt>template</tt> - The template object for the default layout (or +nil+) def _default_layout(formats, require_layout = false) begin value = _layout(formats) if action_has_layout? @@ -426,7 +425,7 @@ module ActionView end def _include_layout?(options) - (options.keys & [:body, :text, :plain, :html, :inline, :partial]).empty? || options.key?(:layout) + (options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout) end end end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index c9f308c2a2..d03e1a51b8 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -51,20 +51,20 @@ module ActionView ActionView::Base.logger end - protected + private EMPTY = "" - def from_rails_root(string) + def from_rails_root(string) # :doc: string = string.sub(rails_root, EMPTY) string.sub!(VIEWS_PATTERN, EMPTY) string end - def rails_root + def rails_root # :doc: @root ||= "#{Rails.root}/" end - def render_count(payload) + def render_count(payload) # :doc: if payload[:cache_hits] "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]" else @@ -72,7 +72,7 @@ module ActionView end end - def cache_message(payload) + def cache_message(payload) # :doc: if payload[:cache_hit] "[cache hit]" else @@ -80,8 +80,6 @@ module ActionView end end - private - def log_rendering_start(payload) info do message = " Rendering #{from_rails_root(payload[:identifier])}" diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 9d6c762cc4..f385a7cd04 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -93,9 +93,9 @@ module ActionView @cache = old_value end - protected + private - def _set_detail(key, value) + def _set_detail(key, value) # :doc: @details = @details.dup if @details_key @details_key = nil @details[key] = value @@ -149,16 +149,16 @@ module ActionView added_resolvers.times { view_paths.pop } end - protected + private - def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc: + def args_for_lookup(name, prefixes, partial, keys, details_options) name, prefixes = normalize_name(name, prefixes) details, details_key = detail_args_for(details_options) [name, prefixes, partial || false, details, details_key, keys] end # Compute details hash and key according to user options (e.g. passed from #render). - def detail_args_for(options) + def detail_args_for(options) # :doc: return @details, details_key if options.empty? # most common path. user_details = @details.merge(options) @@ -171,13 +171,13 @@ module ActionView [user_details, details_key] end - def args_for_any(name, prefixes, partial) # :nodoc: + def args_for_any(name, prefixes, partial) name, prefixes = normalize_name(name, prefixes) details, details_key = detail_args_for_any [name, prefixes, partial || false, details, details_key] end - def detail_args_for_any # :nodoc: + def detail_args_for_any @detail_args_for_any ||= begin details = {} @@ -200,11 +200,11 @@ module ActionView # Support legacy foo.erb names even though we now ignore .erb # as well as incorrectly putting part of the path in the template # name instead of the prefix. - def normalize_name(name, prefixes) #:nodoc: + def normalize_name(name, prefixes) prefixes = prefixes.presence parts = name.to_s.split("/".freeze) parts.shift if parts.first.empty? - name = parts.pop + name = parts.pop return name, prefixes || [""] if parts.empty? diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index dfb99f4ea9..61678933e9 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -3,9 +3,9 @@ require "rails" module ActionView # = Action View Railtie - class Railtie < Rails::Railtie # :nodoc: + class Railtie < Rails::Engine # :nodoc: config.action_view = ActiveSupport::OrderedOptions.new - config.action_view.embed_authenticity_token_in_remote_forms = false + config.action_view.embed_authenticity_token_in_remote_forms = nil config.action_view.debug_missing_translation = true config.eager_load_namespaces << ActionView @@ -17,13 +17,22 @@ module ActionView end end + 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 + end + end + end + initializer "action_view.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end initializer "action_view.set_configs" do |app| ActiveSupport.on_load(:action_view) do - app.config.action_view.each do |k,v| + app.config.action_view.each do |k, v| send "#{k}=", v end end @@ -39,8 +48,8 @@ module ActionView initializer "action_view.per_request_digest_cache" do |app| ActiveSupport.on_load(:action_view) do - if app.config.consider_all_requests_local - app.executor.to_run { ActionView::LookupContext::DetailsKey.clear } + unless ActionView::Resolver.caching? + app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry end end end diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index b39acfa0b5..48bea315a9 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -92,7 +92,7 @@ module ActionView end end - protected + private # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id. # This can be overwritten to customize the default generated string representation if desired. @@ -102,7 +102,7 @@ module ActionView # overwritten version of the method. By default, this implementation passes the key string through a # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to # make sure yourself that your dom ids are valid, in case you overwrite this method. - def record_key_for_dom_id(record) + def record_key_for_dom_id(record) # :doc: key = convert_to_model(record).to_key key ? key.join(JOIN) : key end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 3c85be49cd..0b315eb569 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -25,9 +25,9 @@ module ActionView raise NotImplementedError end - protected + private - def extract_details(options) + def extract_details(options) # :doc: @lookup_context.registered_details.each_with_object({}) do |key, details| value = options[key] @@ -35,7 +35,7 @@ module ActionView end end - def instrument(name, **options) + def instrument(name, **options) # :doc: options[:identifier] ||= (@template && @template.identifier) || @path ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| @@ -43,7 +43,7 @@ module ActionView end end - def prepend_formats(formats) + def prepend_formats(formats) # :doc: formats = Array(formats) return if formats.empty? || @lookup_context.html_fallback_for_js diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index dfe38c488f..647b15ea94 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,4 @@ require "concurrent/map" -require "active_support/core_ext/regexp" require "action_view/renderer/partial_renderer/collection_caching" module ActionView @@ -99,8 +98,8 @@ module ActionView # # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %> # - # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you - # to specify a text which will displayed instead by using this form: + # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return nil. This will allow you + # to specify a text which will be displayed instead by using this form: # # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %> # @@ -358,7 +357,7 @@ module ActionView # set to that string. Otherwise, the +options[:partial]+ object must # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) - @view = context + @view = context @options = options @block = block @@ -459,7 +458,7 @@ module ActionView locals[counter] = index locals[iteration] = partial_iteration - template = (cache[path] ||= find_template(path, keys + [as, counter])) + template = (cache[path] ||= find_template(path, keys + [as, counter, iteration])) content = template.render(view, locals) partial_iteration.iterate! content @@ -533,11 +532,11 @@ module ActionView [variable, variable_counter, variable_iteration] end - IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + + IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \ "make sure your partial name starts with underscore." - OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + + OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ "and is followed by any combination of letters, numbers and underscores." def raise_invalid_identifier(path) diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 1fbe209200..847256ac78 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -38,7 +38,7 @@ module ActionView end def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) + key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 2434250b2d..62ce985243 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -4,7 +4,6 @@ module ActionView # == TODO # # * Support streaming from child templates, partials and so on. - # * Integrate exceptions with exceptron # * Rack::Cache needs to support streaming bodies class StreamingTemplateRenderer < TemplateRenderer #:nodoc: # A valid Rack::Body (i.e. it responds to each). @@ -28,8 +27,7 @@ module ActionView private # This is the same logging logic as in ShowExceptions middleware. - # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron. - def log_error(exception) #:nodoc: + def log_error(exception) logger = ActionView::Base.logger return unless logger diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 4bcf009e27..54317199de 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -22,8 +22,6 @@ module ActionView if options.key?(:body) Template::Text.new(options[:body]) - elsif options.key?(:text) - Template::Text.new(options[:text], formats.first) elsif options.key?(:plain) Template::Text.new(options[:plain]) elsif options.key?(:html) @@ -40,13 +38,13 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option." end end # Renders the given template. A string representing the layout can be # supplied as well. - def render_template(template, layout_name = nil, locals = nil) #:nodoc: + def render_template(template, layout_name = nil, locals = nil) view, locals = @view, locals || {} render_with_layout(layout_name, locals) do |layout| @@ -56,7 +54,7 @@ module ActionView end end - def render_with_layout(path, locals) #:nodoc: + def render_with_layout(path, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 3ca7f9d220..cf18562c45 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -91,7 +91,7 @@ module ActionView # Find and render a template based on the options given. # :api: private - def _render_template(options) #:nodoc: + def _render_template(options) variant = options.delete(:variant) assigns = options.delete(:assigns) context = view_context @@ -104,7 +104,7 @@ module ActionView end # Assign the rendered format to look up context. - def _process_format(format) #:nodoc: + def _process_format(format) super lookup_context.formats = [format.to_sym] lookup_context.rendered_format = lookup_context.formats.first @@ -113,7 +113,7 @@ module ActionView # Normalize args by converting render "foo" to render :action => "foo" and # render "foo/bar" to render :template => "foo/bar". # :api: private - def _normalize_args(action=nil, options={}) + def _normalize_args(action = nil, options = {}) options = super(action, options) case action when NilClass @@ -124,7 +124,11 @@ module ActionView key = action.include?(?/) ? :template : :action options[key] = action else - options[:partial] = action + if action.respond_to?(:permitted?) && action.permitted? + options = action + else + options[:partial] = action + end end options diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 669cffab1a..687ba7c1b4 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -122,18 +122,15 @@ module ActionView controller.url_options end - def _routes_context #:nodoc: - controller - end - protected :_routes_context - - def optimize_routes_generation? #:nodoc: - controller.respond_to?(:optimize_routes_generation?, true) ? - controller.optimize_routes_generation? : super - end - protected :optimize_routes_generation? - private + def _routes_context + controller + end + + def optimize_routes_generation? + controller.respond_to?(:optimize_routes_generation?, true) ? + controller.optimize_routes_generation? : super + end def _generate_paths_by_default true diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 513935cef0..b0e2f1e54e 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -140,7 +140,7 @@ module ActionView end # Returns whether the underlying handler supports streaming. If so, - # a streaming buffer *may* be passed when it start rendering. + # a streaming buffer *may* be passed when it starts rendering. def supports_streaming? handler.respond_to?(:supports_streaming?) && handler.supports_streaming? end @@ -151,7 +151,7 @@ module ActionView # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, buffer=nil, &block) + def render(view, locals, buffer = nil, &block) instrument_render_template do compile!(view) view.send(method_name, locals, buffer, &block) @@ -231,11 +231,11 @@ module ActionView end end - protected + private # Compile a template. This method ensures a template is compiled # just once and removes the source after it is compiled. - def compile!(view) #:nodoc: + def compile!(view) return if @compiled # Templates can be used concurrently in threaded environments @@ -276,9 +276,8 @@ module ActionView # encode the source into <tt>Encoding.default_internal</tt>. # In general, this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. - def compile(mod) #:nodoc: + def compile(mod) encode! - method_name = self.method_name code = @handler.call(self) # Make sure that the resulting String to be eval'd is in the @@ -309,7 +308,7 @@ module ActionView ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) end - def handle_render_error(view, e) #:nodoc: + def handle_render_error(view, e) if e.is_a?(Template::Error) e.sub_template_of(self) raise e @@ -323,12 +322,17 @@ module ActionView end end - def locals_code #:nodoc: + def locals_code + # Only locals with valid variable names get set directly. Others will + # still be available in local_assigns. + locals = @locals - Module::RUBY_RESERVED_KEYWORDS + locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/) + # Double assign to suppress the dreaded 'assigned but unused variable' warning - @locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" } + locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" } end - def method_name #:nodoc: + def method_name @method_name ||= begin m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" m.tr!("-".freeze, "_".freeze) @@ -336,16 +340,14 @@ module ActionView end end - def identifier_method_name #:nodoc: + def identifier_method_name inspect.tr("^a-z_".freeze, "_".freeze) end - def instrument(action, &block) - ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, instrument_payload, &block) + def instrument(action, &block) # :doc: + ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block) end - private - def instrument_render_template(&block) ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, instrument_payload, &block) end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index b95e5236a0..cc90477190 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -1,5 +1,4 @@ require "active_support/core_ext/enumerable" -require "active_support/core_ext/regexp" module ActionView # = Action View Errors @@ -63,23 +62,13 @@ module ActionView # Override to prevent #cause resetting during re-raise. attr_reader :cause - def initialize(template, original_exception = nil) - if original_exception - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize(template) super($!.message) set_backtrace($!.backtrace) @cause = $! @template, @sub_templates = template, nil end - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end - def file_name @template.identifier end diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index e08a5b5db8..67ad78133d 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -1,21 +1,18 @@ module ActionView module Template::Handlers class Builder - # Default format used by Builder. - class_attribute :default_format - self.default_format = :xml + class_attribute :default_format, default: :xml def call(template) require_engine - "xml = ::Builder::XmlMarkup.new(:indent => 2);" + + "xml = ::Builder::XmlMarkup.new(:indent => 2);" \ "self.output_buffer = xml.target!;" + template.source + ";xml.target!;" end - protected - - def require_engine + private + def require_engine # :doc: @required ||= begin require "builder" true diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 6f07de1813..48c2e22a89 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -1,92 +1,21 @@ -require "erubis" -require "active_support/core_ext/regexp" - module ActionView class Template module Handlers - class Erubis < ::Erubis::Eruby - def add_preamble(src) - @newline_pending = 0 - src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" - end - - def add_text(src, text) - return if text.empty? - - if text == "\n" - @newline_pending += 1 - else - src << "@output_buffer.safe_append='" - src << "\n" * @newline_pending if @newline_pending > 0 - src << escape_text(text) - src << "'.freeze;" - - @newline_pending = 0 - end - end - - # Erubis toggles <%= and <%== behavior when escaping is enabled. - # We override to always treat <%== as escaped. - def add_expr(src, code, indicator) - case indicator - when "==" - add_expr_escaped(src, code) - else - super - end - end - - BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ - - def add_expr_literal(src, code) - flush_newline_if_pending(src) - if BLOCK_EXPR.match?(code) - src << "@output_buffer.append= " << code - else - src << "@output_buffer.append=(" << code << ");" - end - end - - def add_expr_escaped(src, code) - flush_newline_if_pending(src) - if BLOCK_EXPR.match?(code) - src << "@output_buffer.safe_expr_append= " << code - else - src << "@output_buffer.safe_expr_append=(" << code << ");" - end - end - - def add_stmt(src, code) - flush_newline_if_pending(src) - super - end - - def add_postamble(src) - flush_newline_if_pending(src) - src << "@output_buffer.to_s" - end - - def flush_newline_if_pending(src) - if @newline_pending > 0 - src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" - @newline_pending = 0 - end - end - end + autoload :Erubis, "action_view/template/handlers/erb/deprecated_erubis" class ERB + autoload :Erubi, "action_view/template/handlers/erb/erubi" + autoload :Erubis, "action_view/template/handlers/erb/erubis" + # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERB documentation for suitable values. - class_attribute :erb_trim_mode - self.erb_trim_mode = "-" + class_attribute :erb_trim_mode, default: "-" # Default implementation used. - class_attribute :erb_implementation - self.erb_implementation = Erubis + class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist - self.escape_whitelist = ["text/plain"] + class_attribute :escape_whitelist, default: ["text/plain"] ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") diff --git a/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb new file mode 100644 index 0000000000..427ea20064 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb @@ -0,0 +1,9 @@ +::ActiveSupport::Deprecation.warn("ActionView::Template::Handlers::Erubis is deprecated and will be removed from Rails 5.2. Switch to ActionView::Template::Handlers::ERB::Erubi instead.") + +module ActionView + class Template + module Handlers + Erubis = ERB::Erubis + end + end +end diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb new file mode 100644 index 0000000000..755cc84015 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -0,0 +1,81 @@ +require "erubi" + +module ActionView + class Template + module Handlers + class ERB + class Erubi < ::Erubi::Engine + # :nodoc: all + def initialize(input, properties = {}) + @newline_pending = 0 + + # Dup properties so that we don't modify argument + properties = Hash[properties] + properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + properties[:postamble] = "@output_buffer.to_s" + properties[:bufvar] = "@output_buffer" + properties[:escapefunc] = "" + + super + end + + def evaluate(action_view_erb_handler_context) + pr = eval("proc { #{@src} }", binding, @filename || "(erubi)") + action_view_erb_handler_context.instance_eval(&pr) + end + + private + def add_text(text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << text.gsub(/['\\]/, '\\\\\&') + src << "'.freeze;" + + @newline_pending = 0 + end + end + + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expression(indicator, code) + flush_newline_if_pending(src) + + if (indicator == "==") || @escape + src << "@output_buffer.safe_expr_append=" + else + src << "@output_buffer.append=" + end + + if BLOCK_EXPR.match?(code) + src << " " << code + else + src << "(" << code << ");" + end + end + + def add_code(code) + flush_newline_if_pending(src) + super + end + + def add_postamble(_) + flush_newline_if_pending(src) + super + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" + @newline_pending = 0 + end + end + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/erb/erubis.rb b/actionview/lib/action_view/template/handlers/erb/erubis.rb new file mode 100644 index 0000000000..f3c35e1aec --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/erubis.rb @@ -0,0 +1,81 @@ +gem "erubis" +require "erubis" + +module ActionView + class Template + module Handlers + class ERB + class Erubis < ::Erubis::Eruby + # :nodoc: all + def add_preamble(src) + @newline_pending = 0 + src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + end + + def add_text(src, text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << escape_text(text) + src << "'.freeze;" + + @newline_pending = 0 + end + end + + # Erubis toggles <%= and <%== behavior when escaping is enabled. + # We override to always treat <%== as escaped. + def add_expr(src, code, indicator) + case indicator + when "==" + add_expr_escaped(src, code) + else + super + end + end + + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expr_literal(src, code) + flush_newline_if_pending(src) + if BLOCK_EXPR.match?(code) + src << "@output_buffer.append= " << code + else + src << "@output_buffer.append=(" << code << ");" + end + end + + def add_expr_escaped(src, code) + flush_newline_if_pending(src) + if BLOCK_EXPR.match?(code) + src << "@output_buffer.safe_expr_append= " << code + else + src << "@output_buffer.safe_expr_append=(" << code << ");" + end + end + + def add_stmt(src, code) + flush_newline_if_pending(src) + super + end + + def add_postamble(src) + flush_newline_if_pending(src) + src << "@output_buffer.to_s" + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" + @newline_pending = 0 + end + end + end + end + end + end +end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 5a2948d5a9..d3905b5f23 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -141,13 +141,13 @@ module ActionView end # Normalizes the arguments and passes it on to find_templates. - def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) + def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = []) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details) end end - def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[]) + def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = []) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details, true) end @@ -164,8 +164,8 @@ module ActionView # This is what child classes implement. No defaults are needed # because Resolver guarantees that the arguments are present and # normalized. - def find_templates(name, prefix, partial, details) - raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method" + def find_templates(name, prefix, partial, details, outside_app_allowed = false) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method" end # Helpers that builds a path. Useful for building virtual paths. @@ -177,7 +177,7 @@ module ActionView # always check the cache before hitting the resolver. Otherwise, # it always hits the resolver but if the key is present, check if the # resolver is fresher before returning it. - def cached(key, path_info, details, locals) #:nodoc: + def cached(key, path_info, details, locals) name, prefix, partial = path_info locals = locals.map(&:to_s).sort! @@ -191,7 +191,7 @@ module ActionView end # Ensures all the resolver information is set in the template. - def decorate(templates, path_info, details, locals) #:nodoc: + def decorate(templates, path_info, details, locals) cached = nil templates.each do |t| t.locals = locals @@ -207,7 +207,7 @@ module ActionView EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." } DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}" - def initialize(pattern=nil) + def initialize(pattern = nil) @pattern = pattern || DEFAULT_PATTERN super() end @@ -226,7 +226,7 @@ module ActionView template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed template_paths.map do |template| - handler, format, variant = extract_handler_and_format_and_variant(template, formats) + handler, format, variant = extract_handler_and_format_and_variant(template) contents = File.binread(template) Template.new(contents, File.expand_path(template), handler, @@ -289,7 +289,7 @@ module ActionView # Extract handler, formats and variant from path. If a format cannot be found neither # from the path, or the handler, we should return the array of formats given # to the resolver. - def extract_handler_and_format_and_variant(path, default_formats) + def extract_handler_and_format_and_variant(path) pieces = File.basename(path).split(".".freeze) pieces.shift @@ -297,7 +297,7 @@ module ActionView handler = Template.handler_for_extension(extension) format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last - format &&= Template::Types[format] + format &&= Template::Types[format] [handler, format, variant] end @@ -342,7 +342,7 @@ module ActionView # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...) # class FileSystemResolver < PathResolver - def initialize(path, pattern=nil) + def initialize(path, pattern = nil) raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver) super(pattern) @path = File.expand_path(path) diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb index e8d4e18f04..380528d6ef 100644 --- a/actionview/lib/action_view/template/text.rb +++ b/actionview/lib/action_view/template/text.rb @@ -4,10 +4,9 @@ module ActionView #:nodoc: class Text #:nodoc: attr_accessor :type - def initialize(string, type = nil) + def initialize(string) @string = string.to_s - @type = Types[type] || type if type - @type ||= Types[:text] + @type = Types[:text] end def identifier @@ -25,7 +24,7 @@ module ActionView #:nodoc: end def formats - [@type.respond_to?(:ref) ? @type.ref : @type.to_s] + [@type.ref] end end end diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 3eb1ac0826..80403799ab 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -18,7 +18,7 @@ module ActionView end def controller_path=(path) - self.class.controller_path=(path) + self.class.controller_path = (path) end def initialize @@ -71,7 +71,7 @@ module ActionView def helper_method(*methods) # Almost a duplicate from ActionController::Helpers methods.flatten.each do |method| - _helpers.module_eval <<-end_eval + _helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def current_user(*args, &block) _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) end # end @@ -124,6 +124,10 @@ module ActionView @_rendered_views ||= RenderedViewsCollection.new end + def _routes + @controller._routes if @controller.respond_to?(:_routes) + end + # Need to experiment if this priority is the best one: rendered => output_buffer class RenderedViewsCollection def initialize @@ -206,8 +210,8 @@ module ActionView view = @controller.view_context view.singleton_class.include(_helpers) view.extend(Locals) - view.rendered_views = self.rendered_views - view.output_buffer = self.output_buffer + view.rendered_views = rendered_views + view.output_buffer = output_buffer view end end @@ -258,10 +262,6 @@ module ActionView end] end - def _routes - @controller._routes if @controller.respond_to?(:_routes) - end - def method_missing(selector, *args) begin routes = @controller.respond_to?(:_routes) && @controller._routes @@ -270,8 +270,8 @@ module ActionView end if routes && - ( routes.named_routes.route_defined?(selector) || - routes.mounted_helpers.method_defined?(selector) ) + (routes.named_routes.route_defined?(selector) || + routes.mounted_helpers.method_defined?(selector)) @controller.__send__(selector, *args) else super diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 5cb9f66529..3188526b63 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -1,4 +1,3 @@ -require "active_support/core_ext/regexp" require "action_view/template/resolver" module ActionView #:nodoc: @@ -9,7 +8,7 @@ module ActionView #:nodoc: class FixtureResolver < PathResolver attr_reader :hash - def initialize(hash = {}, pattern=nil) + def initialize(hash = {}, pattern = nil) super(pattern) @hash = hash end @@ -18,35 +17,35 @@ module ActionView #:nodoc: @hash.keys.join(", ") end - private - - def query(path, exts, formats, _) - query = "" - EXTENSIONS.each_key do |ext| - query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" - end - query = /^(#{Regexp.escape(path)})#{query}$/ - - templates = [] - @hash.each do |_path, array| - source, updated_at = array - next unless query.match?(_path) - handler, format, variant = extract_handler_and_format_and_variant(_path, formats) - templates << Template.new(source, _path, handler, - virtual_path: path.virtual, - format: format, - variant: variant, - updated_at: updated_at - ) + private + + def query(path, exts, _, _) + query = "" + EXTENSIONS.each_key do |ext| + query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" + end + query = /^(#{Regexp.escape(path)})#{query}$/ + + templates = [] + @hash.each do |_path, array| + source, updated_at = array + next unless query.match?(_path) + handler, format, variant = extract_handler_and_format_and_variant(_path) + templates << Template.new(source, _path, handler, + virtual_path: path.virtual, + format: format, + variant: variant, + updated_at: updated_at + ) + end + + templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } end - - templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } - end end class NullResolver < PathResolver - def query(path, exts, formats, _) - handler, format, variant = extract_handler_and_format_and_variant(path, formats) + def query(path, exts, _, _) + handler, format, variant = extract_handler_and_format_and_variant(path) [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant)] end end diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index b5cde5b43f..938f0fc17f 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -3,9 +3,7 @@ module ActionView extend ActiveSupport::Concern included do - class_attribute :_view_paths - self._view_paths = ActionView::PathSet.new - self._view_paths.freeze + class_attribute :_view_paths, default: ActionView::PathSet.new.freeze end delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, @@ -46,10 +44,22 @@ module ActionView {} end + # Append a path to the list of view paths for the current <tt>LookupContext</tt>. + # + # ==== Parameters + # * <tt>path</tt> - If a String is provided, it gets converted into + # the default view path. You may also provide a custom view path + # (see ActionView::PathSet for more information) def append_view_path(path) lookup_context.view_paths.push(*path) end + # Prepend a path to the list of view paths for the current <tt>LookupContext</tt>. + # + # ==== Parameters + # * <tt>path</tt> - If a String is provided, it gets converted into + # the default view path. You may also provide a custom view path + # (see ActionView::PathSet for more information) def prepend_view_path(path) lookup_context.view_paths.unshift(*path) end diff --git a/actionview/package.json b/actionview/package.json new file mode 100644 index 0000000000..85f4ddacbe --- /dev/null +++ b/actionview/package.json @@ -0,0 +1,36 @@ +{ + "name": "rails-ujs", + "version": "5.2.0-alpha", + "description": "Ruby on Rails unobtrusive scripting adapter", + "main": "lib/assets/compiled/rails-ujs.js", + "files": [ + "lib/assets/compiled/*.js" + ], + "directories": { + "test": "test" + }, + "scripts": { + "build": "bundle exec blade build", + "test": "echo \"See the README: https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts#how-to-run-tests\" && exit 1", + "lint": "coffeelint app/assets/javascripts && eslint test/public/test" + }, + "repository": { + "type": "git", + "url": "rails/rails" + }, + "contributors": [ + "Stephen St. Martin", + "Steve Schwartz", + "Dangyi Liu", + "All contributors" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/rails/rails/issues" + }, + "homepage": "http://rubyonrails.org/", + "devDependencies": { + "coffeelint": "^1.15.7", + "eslint": "^2.13.1" + } +} diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 88c7189d22..a7d706c5e1 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -1,16 +1,16 @@ -$:.unshift(File.dirname(__FILE__) + "/lib") -$:.unshift(File.dirname(__FILE__) + "/fixtures/helpers") -$:.unshift(File.dirname(__FILE__) + "/fixtures/alternate_helpers") +$:.unshift File.expand_path("lib", __dir__) +$:.unshift File.expand_path("fixtures/helpers", __dir__) +$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__) -ENV["TMPDIR"] = File.join(File.dirname(__FILE__), "tmp") +ENV["TMPDIR"] = File.expand_path("tmp", __dir__) require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end require "active_support/testing/autorun" @@ -47,7 +47,7 @@ I18n.backend.store_translations "da", {} I18n.backend.store_translations "pt-BR", {} ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort -FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), "fixtures") +FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) module RenderERBUtils def view @@ -133,7 +133,7 @@ class BasicController def config @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| # VIEW TODO: View tests should not require a controller - public_dir = File.expand_path("../fixtures/public", __FILE__) + public_dir = File.expand_path("fixtures/public", __dir__) config.assets_dir = public_dir config.javascripts_dir = "#{public_dir}/javascripts" config.stylesheets_dir = "#{public_dir}/stylesheets" @@ -163,7 +163,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase # Stub Rails dispatcher so it does not get controller references and # simply return the controller#action as Rack::Body. class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher - protected + private def controller_reference(controller_param) controller_param end @@ -196,7 +196,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end def with_autoload_path(path) - path = File.join(File.dirname(__FILE__), "fixtures", path) + path = File.join(File.expand_path("fixtures", __dir__), path) if ActiveSupport::Dependencies.autoload_paths.include?(path) yield else @@ -271,15 +271,15 @@ module ActionDispatch end end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb index a2cd3deb58..8f65a61493 100644 --- a/actionview/test/actionpack/abstract/abstract_controller_test.rb +++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb @@ -42,7 +42,7 @@ module AbstractController super end - append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) + append_view_path File.expand_path("views", __dir__) end class Me2 < RenderingController @@ -152,7 +152,7 @@ module AbstractController class OverridingLocalPrefixes < AbstractController::Base include AbstractController::Rendering include ActionView::Rendering - append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) + append_view_path File.expand_path("views", __dir__) def index render diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb index 5a2f0839e5..13922e4485 100644 --- a/actionview/test/actionpack/abstract/helper_test.rb +++ b/actionview/test/actionpack/abstract/helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -ActionController::Base.helpers_path = File.expand_path("../../../fixtures/helpers", __FILE__) +ActionController::Base.helpers_path = File.expand_path("../../fixtures/helpers", __dir__) module AbstractController module Testing @@ -51,7 +51,7 @@ module AbstractController class AbstractInvalidHelpers < AbstractHelpers include ActionController::Helpers - path = File.expand_path("../../../fixtures/helpers_missing", __FILE__) + path = File.expand_path("../../fixtures/helpers_missing", __dir__) $:.unshift(path) self.helpers_path = path end @@ -109,7 +109,7 @@ module AbstractController class InvalidHelpersTest < ActiveSupport::TestCase def test_controller_raise_error_about_real_require_problem e = assert_raise(LoadError) { AbstractInvalidHelpers.helper(:invalid_require) } - assert_equal "No such file to load -- very_invalid_file_name", e.message + assert_equal "No such file to load -- very_invalid_file_name.rb", e.message end def test_controller_raise_error_about_missing_helper diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb index f0ae609845..cc3a23c60c 100644 --- a/actionview/test/actionpack/controller/capture_test.rb +++ b/actionview/test/actionpack/controller/capture_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" require "active_support/logger" class CaptureController < ActionController::Base - self.view_paths = [ File.dirname(__FILE__) + "/../../fixtures/actionpack" ] + self.view_paths = [ File.expand_path("../../fixtures/actionpack", __dir__) ] def self.controller_name; "test"; end def self.controller_path; "test"; end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 00147d31f3..b3e0329f57 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -1,12 +1,11 @@ require "abstract_unit" require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" # The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited # method has access to the view_paths array when looking for a layout to automatically assign. old_load_paths = ActionController::Base.view_paths -ActionController::Base.view_paths = [ File.dirname(__FILE__) + "/../../fixtures/actionpack/layout_tests/" ] +ActionController::Base.view_paths = [ File.expand_path("../../fixtures/actionpack/layout_tests", __dir__) ] class LayoutTest < ActionController::Base def self.controller_path; "views" end @@ -97,7 +96,7 @@ class StreamingLayoutController < LayoutTest end class AbsolutePathLayoutController < LayoutTest - layout File.expand_path(File.expand_path(__FILE__) + "/../../../fixtures/actionpack/layout_tests/layouts/layout_test") + layout File.expand_path("../../fixtures/actionpack/layout_tests/layouts/layout_test", __dir__) end class HasOwnLayoutController < LayoutTest @@ -118,7 +117,7 @@ end class PrependsViewPathController < LayoutTest def hello - prepend_view_path File.dirname(__FILE__) + "/../../fixtures/actionpack/layout_tests/alt/" + prepend_view_path File.expand_path("../../fixtures/actionpack/layout_tests/alt", __dir__) render layout: "alt" end end @@ -253,7 +252,7 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase end end -unless /mswin|mingw/.match?(RbConfig::CONFIG["host_os"]) +unless Gem.win_platform? class LayoutSymlinkedTest < LayoutTest layout "symlinked/symlinked_layout" end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index cd89dceb45..6528169312 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -1,45 +1,14 @@ require "abstract_unit" require "active_model" +require "controller/fake_models" class ApplicationController < ActionController::Base self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") end -class Customer < Struct.new(:name, :id) - extend ActiveModel::Naming - include ActiveModel::Conversion - - undef_method :to_json - - def to_xml(options={}) - if options[:builder] - options[:builder].name name - else - "<name>#{name}</name>" - end - end - - def to_js(options={}) - "name: #{name.inspect}" - end - alias :to_text :to_js - - def errors - [] - end - - def persisted? - id.present? - end - - def cache_key - name.to_s - end -end - module Quiz #Models - class Question < Struct.new(:name, :id) + Question = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -56,9 +25,6 @@ module Quiz end end -class BadCustomer < Customer; end -class GoodCustomer < Customer; end - module Fun class GamesController < ApplicationController def hello_world; end @@ -90,7 +56,7 @@ class TestController < ApplicationController end def hello_world_file - render file: File.expand_path("../../../fixtures/actionpack/hello", __FILE__), formats: [:html] + render file: File.expand_path("../../fixtures/actionpack/hello", __dir__), formats: [:html] end # :ported: @@ -159,7 +125,7 @@ class TestController < ApplicationController # :ported: def render_file_with_instance_variables @secret = "in the sauce" - path = File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_ivar") + path = File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) render file: path end @@ -176,21 +142,21 @@ class TestController < ApplicationController def render_file_using_pathname @secret = "in the sauce" - render file: Pathname.new(File.dirname(__FILE__)).join("..", "..", "fixtures", "test", "dot.directory", "render_file_with_ivar") + render file: Pathname.new(__dir__).join("..", "..", "fixtures", "test", "dot.directory", "render_file_with_ivar") end def render_file_from_template @secret = "in the sauce" - @path = File.expand_path(File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_ivar")) + @path = File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) end def render_file_with_locals - path = File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_locals") + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) render file: path, locals: { secret: "in the sauce" } end def render_file_as_string_with_locals - path = File.expand_path(File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_locals")) + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) render file: path, locals: { secret: "in the sauce" } end diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index 7f94b7ebb4..901c0e2b3e 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -13,7 +13,7 @@ end # Try to grab AR unless defined?(ActiveRecord) && defined?(FixtureSet) begin - PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib" + PATH_TO_AR = File.expand_path("../../activerecord/lib", __dir__) raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR) $LOAD_PATH.unshift PATH_TO_AR require "active_record" @@ -58,13 +58,13 @@ class ActiveRecordTestConnector # Load actionpack sqlite3 tables def load_schema - File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(";").each do |sql| + File.read(File.expand_path("fixtures/db_definitions/sqlite.sql", __dir__)).split(";").each do |sql| ActiveRecord::Base.connection.execute(sql) unless sql.blank? end end def require_fixture_models - Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each { |f| require f } + Dir.glob(File.expand_path("fixtures/*.rb", __dir__)).each { |f| require f } end end end diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 590559f592..1cec5072c0 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -67,7 +67,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0.0ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0\.0ms\)/, @logger.logged(:info)[1]) end def test_log_with_active_record_when_post diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 6152ec4720..3b314588c7 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -45,14 +45,14 @@ class FormHelperActiveRecordTest < ActionView::TestCase end expected = whole_form("/developers/123", "edit_developer_123", "edit_developer", method: "patch") do - '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' + + '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' \ '<input id="developer_projects_attributes_abc_id" name="developer[projects_attributes][abc][id]" type="hidden" value="321" />' end assert_dom_equal expected, output_buffer end - protected + private def hidden_fields(method = nil) txt = %{<input name="utf8" type="hidden" value="✓" />} diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index 8495949975..b2e0fb08c4 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -45,7 +45,7 @@ class ModelDelegate end end -module Blog +module Weblog class Post < ActiveRecord::Base self.table_name = "projects" end @@ -61,7 +61,7 @@ end class PolymorphicRoutesTest < ActionController::TestCase include SharedTestRoutes.url_helpers - self.default_url_options[:host] = "example.com" + default_url_options[:host] = "example.com" def setup @project = Project.new @@ -72,8 +72,8 @@ class PolymorphicRoutesTest < ActionController::TestCase @fax = Fax.new @delegator = ModelDelegator.new @series = Series.new - @blog_post = Blog::Post.new - @blog_blog = Blog::Blog.new + @blog_post = Weblog::Post.new + @blog_blog = Weblog::Blog.new end def assert_url(url, args) @@ -86,8 +86,7 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_string with_test_routes do - # FIXME: why are these different? Symbol case passes through to - # `polymorphic_url`, but the String case doesn't. + assert_equal "/projects", polymorphic_path("projects") assert_equal "http://example.com/projects", polymorphic_url("projects") assert_equal "projects", url_for("projects") end @@ -731,3 +730,49 @@ class PolymorphicPathRoutesTest < PolymorphicRoutesTest assert_equal url.sub(/http:\/\/#{host}/, ""), url_for(args) end end + +class DirectRoutesTest < ActionView::TestCase + class Linkable + attr_reader :id + + def self.name + super.demodulize + end + + def initialize(id) + @id = id + end + + def linkable_type + self.class.name.underscore + end + end + + class Category < Linkable; end + class Collection < Linkable; end + class Product < Linkable; end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + resources :categories, :collections, :products + direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] } + end + + include Routes.url_helpers + + def setup + @category = Category.new("1") + @collection = Collection.new("2") + @product = Product.new("3") + end + + def test_direct_routes + assert_equal "/categories/1", linkable_path(@category) + assert_equal "/collections/2", linkable_path(@collection) + assert_equal "/products/3", linkable_path(@product) + + assert_equal "http://test.host/categories/1", linkable_url(@category) + assert_equal "http://test.host/collections/2", linkable_url(@collection) + assert_equal "http://test.host/products/3", linkable_url(@product) + end +end diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index 880e80a8dc..fbab512c41 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -1,6 +1,6 @@ require "active_record_unit" -class RelationCacheTest < ActionView::TestCase +class RelationCacheTest < ActionView::TestCase tests ActionView::Helpers::CacheHelper def setup @@ -10,7 +10,7 @@ class RelationCacheTest < ActionView::TestCase def test_cache_relation_other cache(Project.all) { concat("Hello World") } - assert_equal "Hello World", controller.cache_store.read("views/projects-#{Project.count}/") + assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}") end def view_cache_dependencies; end diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb index 55886da30f..60c3ab3045 100644 --- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb @@ -87,7 +87,7 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase end end -class Game < Struct.new(:name, :id) +Game = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion def to_param diff --git a/actionview/test/fixtures/actionpack/layouts/builder.builder b/actionview/test/fixtures/actionpack/layouts/builder.builder index 7c7d4b2dd1..c55488edd0 100644 --- a/actionview/test/fixtures/actionpack/layouts/builder.builder +++ b/actionview/test/fixtures/actionpack/layouts/builder.builder @@ -1,3 +1,3 @@ xml.wrapper do xml << yield -end
\ No newline at end of file +end diff --git a/actionview/test/fixtures/actionpack/test/_hello.builder b/actionview/test/fixtures/actionpack/test/_hello.builder index ef52f632d1..fc72df16d0 100644 --- a/actionview/test/fixtures/actionpack/test/_hello.builder +++ b/actionview/test/fixtures/actionpack/test/_hello.builder @@ -1 +1 @@ -xm.hello
\ No newline at end of file +xm.hello diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder index 14fd3549fb..f98aaa34a5 100644 --- a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder +++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder @@ -1 +1 @@ -xml.test 'failed'
\ No newline at end of file +xml.test "failed" diff --git a/actionview/test/fixtures/actionpack/test/hello.builder b/actionview/test/fixtures/actionpack/test/hello.builder index a471553941..b8ab17ad5b 100644 --- a/actionview/test/fixtures/actionpack/test/hello.builder +++ b/actionview/test/fixtures/actionpack/test/hello.builder @@ -1,4 +1,4 @@ xml.html do xml.p "Hello #{@name}" - xml << render(:file => "test/greeting") -end
\ No newline at end of file + xml << render(file: "test/greeting") +end diff --git a/actionview/test/fixtures/actionpack/test/hello_world_container.builder b/actionview/test/fixtures/actionpack/test/hello_world_container.builder index e48d75c405..24032ab5e0 100644 --- a/actionview/test/fixtures/actionpack/test/hello_world_container.builder +++ b/actionview/test/fixtures/actionpack/test/hello_world_container.builder @@ -1,3 +1,3 @@ xml.test do - render :partial => 'hello', :locals => { :xm => xml } -end
\ No newline at end of file + render partial: "hello", locals: { xm: xml } +end diff --git a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder index e7081b89fe..d16bb6b5cb 100644 --- a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder +++ b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder @@ -8,4 +8,4 @@ xml.html do xml.p "monks" xml.p "wiseguys" end -end
\ No newline at end of file +end diff --git a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder index d539a425a4..cd65da751b 100644 --- a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder +++ b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder @@ -1,4 +1,4 @@ content_for :title do - 'Putting stuff in the title!' + "Putting stuff in the title!" end xml << "Great stuff!" diff --git a/actionview/test/fixtures/comments/empty.html+grid.erb b/actionview/test/fixtures/comments/empty.html+grid.erb new file mode 100644 index 0000000000..dc3fa32a81 --- /dev/null +++ b/actionview/test/fixtures/comments/empty.html+grid.erb @@ -0,0 +1 @@ +<h1>No Comment</h1> diff --git a/actionview/test/fixtures/comments/empty.html.builder b/actionview/test/fixtures/comments/empty.html.builder index 2b0c7207a3..12d6fdd9a5 100644 --- a/actionview/test/fixtures/comments/empty.html.builder +++ b/actionview/test/fixtures/comments/empty.html.builder @@ -1 +1 @@ -xml.h1 'No Comment'
\ No newline at end of file +xml.h1 "No Comment" diff --git a/actionview/test/fixtures/test/_partial_iteration_1.erb b/actionview/test/fixtures/test/_partial_iteration_1.erb new file mode 100644 index 0000000000..c0fdd4c22a --- /dev/null +++ b/actionview/test/fixtures/test/_partial_iteration_1.erb @@ -0,0 +1 @@ +<%= defined?(partial_iteration_1_iteration) %> diff --git a/actionview/test/fixtures/test/_partial_iteration_2.erb b/actionview/test/fixtures/test/_partial_iteration_2.erb new file mode 100644 index 0000000000..50dd11db27 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_iteration_2.erb @@ -0,0 +1 @@ +<%= defined?(partial_iteration_2_iteration) -%> diff --git a/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb b/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb new file mode 100644 index 0000000000..225363c8c3 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb @@ -0,0 +1 @@ +<h1>Partial with variants</h1> diff --git a/actionview/test/fixtures/test/hello.builder b/actionview/test/fixtures/test/hello.builder index a471553941..b8ab17ad5b 100644 --- a/actionview/test/fixtures/test/hello.builder +++ b/actionview/test/fixtures/test/hello.builder @@ -1,4 +1,4 @@ xml.html do xml.p "Hello #{@name}" - xml << render(:file => "test/greeting") -end
\ No newline at end of file + xml << render(file: "test/greeting") +end diff --git a/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb b/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb new file mode 100644 index 0000000000..aea5c351c5 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb @@ -0,0 +1 @@ +<%= local_assigns.inspect.html_safe %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/render_file_instance_variable.erb b/actionview/test/fixtures/test/render_file_instance_variable.erb new file mode 100644 index 0000000000..5344ac8a66 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_instance_variable.erb @@ -0,0 +1 @@ +<%= @foo %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/render_file_unicode_local.erb b/actionview/test/fixtures/test/render_file_unicode_local.erb new file mode 100644 index 0000000000..cbfd040a76 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_unicode_local.erb @@ -0,0 +1 @@ +<%= 🎃 %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb b/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb new file mode 100644 index 0000000000..7e3fe6c6d9 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb @@ -0,0 +1 @@ +The class is <%= local_assigns[:class] %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb b/actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb new file mode 100644 index 0000000000..edfe52e422 --- /dev/null +++ b/actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb @@ -0,0 +1 @@ +<%= _ %> <%= arg %> <%= args %> <%= block %>
\ No newline at end of file diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb index cc5f5c1d59..5250101220 100644 --- a/actionview/test/lib/controller/fake_models.rb +++ b/actionview/test/lib/controller/fake_models.rb @@ -1,12 +1,12 @@ require "active_model" -class Customer < Struct.new(:name, :id) +Customer = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion undef_method :to_json - def to_xml(options={}) + def to_xml(options = {}) if options[:builder] options[:builder].name name else @@ -14,7 +14,7 @@ class Customer < Struct.new(:name, :id) end end - def to_js(options={}) + def to_js(options = {}) "name: #{name.inspect}" end alias :to_text :to_js @@ -26,12 +26,16 @@ class Customer < Struct.new(:name, :id) def persisted? id.present? end -end -class GoodCustomer < Customer + def cache_key + name.to_s + end end -class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) +class BadCustomer < Customer; end +class GoodCustomer < Customer; end + +Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do extend ActiveModel::Naming include ActiveModel::Conversion extend ActiveModel::Translation @@ -80,7 +84,7 @@ class Comment def to_key; id ? [id] : nil end def save; @id = 1; @post_id = 1 end def persisted?; @id.present? end - def to_param; @id.to_s; end + def to_param; @id && @id.to_s; end def name @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" end @@ -101,7 +105,7 @@ class Tag def to_key; id ? [id] : nil end def save; @id = 1; @post_id = 1 end def persisted?; @id.present? end - def to_param; @id; end + def to_param; @id && @id.to_s; end def value @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" end @@ -120,7 +124,7 @@ class CommentRelevance def to_key; id ? [id] : nil end def save; @id = 1; @comment_id = 1 end def persisted?; @id.present? end - def to_param; @id; end + def to_param; @id && @id.to_s; end def value @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" end @@ -136,7 +140,7 @@ class TagRelevance def to_key; id ? [id] : nil end def save; @id = 1; @tag_id = 1 end def persisted?; @id.present? end - def to_param; @id; end + def to_param; @id && @id.to_s; end def value @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" end @@ -163,7 +167,7 @@ module Blog true end - class Post < Struct.new(:title, :id) + Post = Struct.new(:title, :id) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -183,8 +187,7 @@ class ArelLike end end -class Car < Struct.new(:color) -end +Car = Struct.new(:color) class Plane attr_reader :to_key diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb index 8b8f686f96..6b63aa25a5 100644 --- a/actionview/test/template/active_model_helper_test.rb +++ b/actionview/test/template/active_model_helper_test.rb @@ -4,7 +4,7 @@ class ActiveModelHelperTest < ActionView::TestCase tests ActionView::Helpers::ActiveModelHelper silence_warnings do - class Post < Struct.new(:author_name, :body, :updated_at) + Post = Struct.new(:author_name, :body, :updated_at) do include ActiveModel::Conversion include ActiveModel::Validations diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 3bdab42f7a..6093a4e660 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -53,6 +53,7 @@ class AssetTagHelperTest < ActionView::TestCase %(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />), + %(auto_discovery_link_tag(:json)) => %(<link href="http://www.example.com" rel="alternate" title="JSON" type="application/json" />), %(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), @@ -630,7 +631,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase end def test_should_return_nothing_if_asset_host_isnt_configured - assert_equal nil, compute_asset_host("foo") + assert_nil compute_asset_host("foo") end def test_should_current_request_host_is_always_returned_for_request @@ -709,13 +710,13 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase def test_should_wildcard_asset_host @controller.config.asset_host = "http://a%d.example.com" - assert_match(%r(http://a[0123].example.com), compute_asset_host("foo")) + assert_match(%r(http://a[0123]\.example\.com), compute_asset_host("foo")) end def test_should_wildcard_asset_host_between_zero_and_four @controller.config.asset_host = "http://a%d.example.com" - assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) - assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_url("xml.png")) + assert_match(%r(http://a[0123]\.example\.com/collaboration/hieraki/images/xml\.png), image_path("xml.png")) + assert_match(%r(http://a[0123]\.example\.com/collaboration/hieraki/images/xml\.png), image_url("xml.png")) end def test_asset_host_without_protocol_should_be_protocol_relative diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index e9a923dd72..7304b769a4 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) +Scroll = Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -301,8 +301,8 @@ class AtomFeedTest < ActionController::TestCase with_restful_routing(:scrolls) do get :index, params: { id: "feed_with_atomPub_namespace" } assert_match %r{xml:lang="en-US"}, @response.body - assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body - assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body + assert_match %r{xmlns="http://www\.w3\.org/2005/Atom"}, @response.body + assert_match %r{xmlns:app="http://www\.w3\.org/2007/app"}, @response.body end end @@ -319,7 +319,7 @@ class AtomFeedTest < ActionController::TestCase with_restful_routing(:scrolls) do get :index, params: { id: "feed_with_xml_processing_instructions" } assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body - assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body + assert_match %r{<\?xml-stylesheet [^\?]*href="t\.css"}, @response.body end end @@ -334,7 +334,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_xhtml with_restful_routing(:scrolls) do get :index, params: { id: "feed_with_xhtml_content" } - assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body + assert_match %r{xmlns="http://www\.w3\.org/1999/xhtml"}, @response.body assert_select "summary", text: /Something Boring/ assert_select "summary", text: /after 2/ end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 54bf9b4c33..7f37523eeb 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -127,18 +127,18 @@ class CaptureHelperTest < ActionView::TestCase def test_content_for_returns_nil_when_writing assert ! content_for?(:title) - assert_equal nil, content_for(:title, "foo") - assert_equal nil, content_for(:title) { output_buffer << "bar"; nil } - assert_equal nil, content_for(:title) { output_buffer << " \n "; nil } + assert_nil content_for(:title, "foo") + assert_nil content_for(:title) { output_buffer << "bar"; nil } + assert_nil content_for(:title) { output_buffer << " \n "; nil } assert_equal "foobar", content_for(:title) - assert_equal nil, content_for(:title, "foo", flush: true) - assert_equal nil, content_for(:title, flush: true) { output_buffer << "bar"; nil } - assert_equal nil, content_for(:title, flush: true) { output_buffer << " \n "; nil } + assert_nil content_for(:title, "foo", flush: true) + assert_nil content_for(:title, flush: true) { output_buffer << "bar"; nil } + assert_nil content_for(:title, flush: true) { output_buffer << " \n "; nil } assert_equal "bar", content_for(:title) end def test_content_for_returns_nil_when_content_missing - assert_equal nil, content_for(:some_missing_key) + assert_nil content_for(:some_missing_key) end def test_content_for_question_mark diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb index 7e3e5883b4..adb2be9be4 100644 --- a/actionview/test/template/compiled_templates_test.rb +++ b/actionview/test/template/compiled_templates_test.rb @@ -9,6 +9,39 @@ class CompiledTemplatesTest < ActiveSupport::TestCase assert_equal "This is nil: \n", render(template: "test/nil_return") end + def test_template_with_ruby_keyword_locals + assert_equal "The class is foo", + render(file: "test/render_file_with_ruby_keyword_locals", locals: { class: "foo" }) + end + + def test_template_with_invalid_identifier_locals + locals = { + foo: "bar", + Foo: "bar", + "d-a-s-h-e-s": "", + "white space": "", + } + assert_equal locals.inspect, render(file: "test/render_file_inspect_local_assigns", locals: locals) + end + + def test_template_with_delegation_reserved_keywords + locals = { + _: "one", + arg: "two", + args: "three", + block: "four", + } + assert_equal "one two three four", render(file: "test/test_template_with_delegation_reserved_keywords", locals: locals) + end + + def test_template_with_unicode_identifier + assert_equal "🎂", render(file: "test/render_file_unicode_local", locals: { 🎃: "🎂" }) + end + + def test_template_with_instance_variable_identifier + assert_equal "bar", render(file: "test/render_file_instance_variable", locals: { "@foo": "bar" }) + end + def test_template_gets_recompiled_when_using_different_keys_in_local_assigns assert_equal "one", render(file: "test/render_file_with_locals_and_default") assert_equal "two", render(file: "test/render_file_with_locals_and_default", locals: { secret: "two" }) diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 44e5a8c346..b667303318 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -18,7 +18,7 @@ class DateHelperTest < ActionView::TestCase end end - def assert_distance_of_time_in_words(from, to=nil) + def assert_distance_of_time_in_words(from, to = nil) to ||= from # 0..1 minute with :include_seconds => true @@ -128,11 +128,29 @@ class DateHelperTest < ActionView::TestCase assert_distance_of_time_in_words(from) end - def test_distance_in_words_with_mathn_required - # test we avoid Integer#/ (redefined by mathn) - silence_warnings { require "mathn" } + def test_distance_in_words_with_nil_input + assert_raises(ArgumentError) { distance_of_time_in_words(nil) } + assert_raises(ArgumentError) { distance_of_time_in_words(0, nil) } + end + + def test_distance_in_words_with_mixed_argument_types + assert_equal "1 minute", distance_of_time_in_words(0, Time.at(60)) + assert_equal "10 minutes", distance_of_time_in_words(Time.at(600), 0) + end + + def test_distance_in_words_doesnt_use_the_quotient_operator + rubinius_skip "Date is written in Ruby and relies on Fixnum#/" + jruby_skip "Date is written in Ruby and relies on Fixnum#/" + + klass = RUBY_VERSION > "2.4" ? Integer : Fixnum + + # Make sure that we avoid {Integer,Fixnum}#/ (redefined by mathn) + klass.send :private, :/ + from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) + ensure + klass.send :public, :/ end def test_time_ago_in_words_passes_include_seconds @@ -167,9 +185,9 @@ class DateHelperTest < ActionView::TestCase def test_distance_in_words_with_integers assert_equal "1 minute", distance_of_time_in_words(59) - assert_equal "about 1 hour", distance_of_time_in_words(60*60) + assert_equal "about 1 hour", distance_of_time_in_words(60 * 60) assert_equal "1 minute", distance_of_time_in_words(0, 59) - assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0) + assert_equal "about 1 hour", distance_of_time_in_words(60 * 60, 0) assert_equal "about 3 years", distance_of_time_in_words(10**8) assert_equal "about 3 years", distance_of_time_in_words(0, 10**8) end @@ -385,7 +403,7 @@ class DateHelperTest < ActionView::TestCase month_names = %w(Januar Februar Marts April Maj Juni Juli August September Oktober November December) expected = %(<select id="date_month" name="date[month]">\n) - 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month-1]}</option>\n) } + 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month - 1]}</option>\n) } expected << "</select>\n" assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), use_month_names: month_names) @@ -832,7 +850,7 @@ class DateHelperTest < ActionView::TestCase def test_select_date_with_too_big_range_between_start_year_and_end_year assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), start_year: 2000, end_year: 20000, prefix: "date[first]", order: [:month, :day, :year]) } - assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), start_year: Date.today.year - 100.years, end_year: 2000, prefix: "date[first]", order: [:month, :day, :year]) } + assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), start_year: 100, end_year: 2000, prefix: "date[first]", order: [:month, :day, :year]) } end def test_select_date_can_have_more_then_1000_years_interval_if_forced_via_parameter @@ -848,7 +866,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" - expected << %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -881,8 +899,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_no_start_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+1) do |y| + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 1) do |y| if y == Date.today.year expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) else @@ -900,12 +918,12 @@ class DateHelperTest < ActionView::TestCase expected << "</select>\n" assert_dom_equal expected, select_date( - Time.mktime(Date.today.year, 8, 16), end_year: Date.today.year+1, prefix: "date[first]" + Time.mktime(Date.today.year, 8, 16), end_year: Date.today.year + 1, prefix: "date[first]" ) end def test_select_date_with_no_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected = %(<select id="date_first_year" name="date[first][year]">\n) 2003.upto(2008) do |y| if y == 2003 expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) @@ -929,8 +947,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_no_start_or_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) do |y| + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) do |y| if y == Date.today.year expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) else @@ -969,8 +987,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_zero_value_and_no_start_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -981,7 +999,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" - assert_dom_equal expected, select_date(0, end_year: Date.today.year+1, prefix: "date[first]") + assert_dom_equal expected, select_date(0, end_year: Date.today.year + 1, prefix: "date[first]") end def test_select_date_with_zero_value_and_no_end_year @@ -1002,8 +1020,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_zero_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -1018,8 +1036,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -1286,8 +1304,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -1407,7 +1425,6 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_custom_prompt - expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -1709,7 +1726,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_generic_with_css_classes - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1733,7 +1750,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_custom_with_css_classes - expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) @@ -1851,7 +1868,7 @@ class DateHelperTest < ActionView::TestCase expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" - expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} expected << "</select>\n" @@ -1882,7 +1899,7 @@ class DateHelperTest < ActionView::TestCase expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" - expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} expected << "</select>\n" @@ -1901,7 +1918,7 @@ class DateHelperTest < ActionView::TestCase expected = "<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n" - expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n} + expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} expected << "</select>\n" @@ -2003,7 +2020,7 @@ class DateHelperTest < ActionView::TestCase @post = Post.new @post.written_on = Date.new(2004, 6, 15) - expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } expected << "</select>\n" @@ -2011,7 +2028,7 @@ class DateHelperTest < ActionView::TestCase 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } expected << "</select>\n" - expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } expected << "</select>\n" @@ -2021,8 +2038,8 @@ class DateHelperTest < ActionView::TestCase def test_date_select_with_nil @post = Post.new - start_year = Time.now.year-5 - end_year = Time.now.year+5 + start_year = Time.now.year - 5 + end_year = Time.now.year + 5 expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} start_year.upto(end_year) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) } expected << "</select>\n" @@ -2041,8 +2058,8 @@ class DateHelperTest < ActionView::TestCase def test_date_select_with_nil_and_blank @post = Post.new - start_year = Time.now.year-5 - end_year = Time.now.year+5 + start_year = Time.now.year - 5 + end_year = Time.now.year + 5 expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << "<option value=\"\"></option>\n" start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } @@ -2064,11 +2081,11 @@ class DateHelperTest < ActionView::TestCase def test_date_select_with_nil_and_blank_and_order @post = Post.new - start_year = Time.now.year-5 - end_year = Time.now.year+5 + start_year = Time.now.year - 5 + end_year = Time.now.year + 5 expected = '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n" - expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << "<option value=\"\"></option>\n" start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } expected << "</select>\n" @@ -2084,8 +2101,8 @@ class DateHelperTest < ActionView::TestCase def test_date_select_with_nil_and_blank_and_discard_month @post = Post.new - start_year = Time.now.year-5 - end_year = Time.now.year+5 + start_year = Time.now.year - 5 + end_year = Time.now.year + 5 expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << "<option value=\"\"></option>\n" @@ -2782,7 +2799,7 @@ class DateHelperTest < ActionView::TestCase def test_datetime_select_with_infinity # Float @post = Post.new - @post.updated_at = (-1.0/0) + @post.updated_at = (-1.0 / 0) datetime_select("post", "updated_at") end @@ -2903,8 +2920,8 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_zero_value_and_no_start_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -2915,7 +2932,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" - assert_dom_equal expected, select_date(0, end_year: Date.today.year+1, prefix: "date[first]") + assert_dom_equal expected, select_date(0, end_year: Date.today.year + 1, prefix: "date[first]") end def test_date_select_with_zero_value_and_no_end_year @@ -2936,8 +2953,8 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_zero_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -2952,8 +2969,8 @@ class DateHelperTest < ActionView::TestCase end def test_date_select_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) @@ -2968,8 +2985,8 @@ class DateHelperTest < ActionView::TestCase end def test_datetime_select_with_nil_value_and_no_start_and_end_year - expected = %(<select id="date_first_year" name="date[first][year]">\n) - (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } expected << "</select>\n" expected << %(<select id="date_first_month" name="date[first][month]">\n) diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 093ff28c14..de04f3f25d 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -14,7 +14,7 @@ class FixtureTemplate end class FixtureFinder < ActionView::LookupContext - FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" + FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__) def initialize(details = {}) super(ActionView::PathSet.new(["digestor", "digestor/api"]), details, []) @@ -122,13 +122,13 @@ class TemplateDigestorTest < ActionView::TestCase end def test_logging_of_missing_template_for_dependencies - assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do + assert_logged "Couldn't find template for digesting: messages/something_missing" do dependencies("messages/something_missing") end end def test_logging_of_missing_template_for_nested_dependencies - assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do + assert_logged "Couldn't find template for digesting: messages/something_missing" do nested_dependencies("messages/something_missing") end end @@ -139,7 +139,7 @@ class TemplateDigestorTest < ActionView::TestCase end def test_getting_of_doubly_nested_dependencies - doubly_nested = [{ "comments/comments"=>["comments/comment"] }, "messages/message"] + doubly_nested = [{ "comments/comments" => ["comments/comment"] }, "messages/message"] assert_equal doubly_nested, nested_dependencies("messages/peek") end @@ -150,13 +150,13 @@ class TemplateDigestorTest < ActionView::TestCase end def test_nested_template_deps - nested_deps = ["messages/header", { "comments/comments"=>["comments/comment"] }, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"] + nested_deps = ["messages/header", { "comments/comments" => ["comments/comment"] }, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"] assert_equal nested_deps, nested_dependencies("messages/show") end def test_nested_template_deps_with_non_default_rendered_format finder.rendered_format = nil - nested_deps = [{ "comments/comments"=>["comments/comment"] }] + nested_deps = [{ "comments/comments" => ["comments/comment"] }] assert_equal nested_deps, nested_dependencies("messages/thread") end diff --git a/actionview/test/template/erb/deprecated_erubis_implementation_test.rb b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb new file mode 100644 index 0000000000..aaf99f85c0 --- /dev/null +++ b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb @@ -0,0 +1,13 @@ +require "abstract_unit" + +module ERBTest + class DeprecatedErubisImplementationTest < ActionView::TestCase + test "Erubis implementation is deprecated" do + assert_deprecated "ActionView::Template::Handlers::Erubis is deprecated and will be removed from Rails 5.2. Switch to ActionView::Template::Handlers::ERB::Erubi instead." do + assert_equal "ActionView::Template::Handlers::ERB::Erubis", ActionView::Template::Handlers::Erubis.to_s + + assert_nothing_raised { Class.new(ActionView::Template::Handlers::Erubis) } + end + end + end +end diff --git a/actionview/test/template/erb/helper.rb b/actionview/test/template/erb/helper.rb index a1973068d5..bc1eedc15f 100644 --- a/actionview/test/template/erb/helper.rb +++ b/actionview/test/template/erb/helper.rb @@ -14,7 +14,7 @@ module ERBTest class BlockTestCase < ActiveSupport::TestCase def render_content(start, inside) template = block_helper(start, inside) - ActionView::Template::Handlers::Erubis.new(template).evaluate(ViewContext.new) + ActionView::Template::Handlers::ERB.erb_implementation.new(template).evaluate(ViewContext.new) end def block_helper(str, rest) diff --git a/actionview/test/template/erb/tag_helper_test.rb b/actionview/test/template/erb/tag_helper_test.rb index 84e328d8be..233f37c48a 100644 --- a/actionview/test/template/erb/tag_helper_test.rb +++ b/actionview/test/template/erb/tag_helper_test.rb @@ -18,8 +18,8 @@ module ERBTest end test "percent equals works with form tags" do - expected_output = %r{<form.*action="foo".*method="post">.*hello*</form>} - assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") + expected_output = %r{<form.*action="/foo".*method="post">.*hello*</form>} + assert_match expected_output, render_content("form_tag('/foo')", "<%= 'hello' %>") end test "percent equals works with fieldset tags" do diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb index 0a16364ed3..4412ea37fa 100644 --- a/actionview/test/template/erb_util_test.rb +++ b/actionview/test/template/erb_util_test.rb @@ -51,7 +51,12 @@ class ErbUtilTest < ActiveSupport::TestCase def test_json_escape_does_not_alter_json_string_meaning JSON_ESCAPE_TEST_CASES.each do |(raw, _)| - assert_equal ActiveSupport::JSON.decode(raw), ActiveSupport::JSON.decode(json_escape(raw)) + expected = ActiveSupport::JSON.decode(raw) + if expected.nil? + assert_nil ActiveSupport::JSON.decode(json_escape(raw)) + else + assert_equal expected, ActiveSupport::JSON.decode(json_escape(raw)) + end end end diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index 3774dcf872..6160524eb3 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -1,7 +1,6 @@ require "abstract_unit" -class Category < Struct.new(:id, :name) -end +Category = Struct.new(:id, :name) class FormCollectionsHelperTest < ActionView::TestCase def assert_no_select(selector, value = nil) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb new file mode 100644 index 0000000000..bff0643fb0 --- /dev/null +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -0,0 +1,2255 @@ +require "abstract_unit" +require "controller/fake_models" + +class FormWithTest < ActionView::TestCase + include RenderERBUtils +end + +class FormWithActsLikeFormTagTest < FormWithTest + tests ActionView::Helpers::FormTagHelper + + setup do + @controller = BasicController.new + end + + def hidden_fields(options = {}) + method = options[:method] + skip_enforcing_utf8 = options.fetch(:skip_enforcing_utf8, false) + + "".tap do |txt| + unless skip_enforcing_utf8 + txt << %{<input name="utf8" type="hidden" value="✓" />} + end + + if method && !%w(get post).include?(method.to_s) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end + end + end + + def form_text(action = "http://www.example.com", local: false, **options) + enctype, html_class, id, method = options.values_at(:enctype, :html_class, :id, :method) + + method = method.to_s == "get" ? "get" : "post" + + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if enctype + txt << %{ data-remote="true"} unless local + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + txt << %{ method="#{method}">} + end + + def whole_form(action = "http://www.example.com", options = {}) + out = form_text(action, options) + hidden_fields(options) + + if block_given? + out << yield << "</form>" + end + + out + end + + def url_for(options) + if options.is_a?(Hash) + "http://www.example.com" + else + super + end + end + + def test_form_with_multipart + actual = form_with(multipart: true) + + expected = whole_form("http://www.example.com", enctype: true) + assert_dom_equal expected, actual + end + + def test_form_with_with_method_patch + actual = form_with(method: :patch) + + expected = whole_form("http://www.example.com", method: :patch) + assert_dom_equal expected, actual + end + + def test_form_with_with_method_put + actual = form_with(method: :put) + + expected = whole_form("http://www.example.com", method: :put) + assert_dom_equal expected, actual + end + + def test_form_with_with_method_delete + actual = form_with(method: :delete) + + expected = whole_form("http://www.example.com", method: :delete) + assert_dom_equal expected, actual + end + + def test_form_with_with_local_true + actual = form_with(local: true) + + expected = whole_form("http://www.example.com", local: true) + assert_dom_equal expected, actual + end + + def test_form_with_skip_enforcing_utf8_true + 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? + end + + def test_form_with_with_block_in_erb + output_buffer = render_erb("<%= form_with(url: 'http://www.example.com') do %>Hello world!<% end %>") + + expected = whole_form { "Hello world!" } + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_block_and_method_in_erb + output_buffer = render_erb("<%= form_with(url: 'http://www.example.com', method: :put) do %>Hello world!<% end %>") + + expected = whole_form("http://www.example.com", method: "put") do + "Hello world!" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_block_in_erb_and_local_true + output_buffer = render_erb("<%= form_with(url: 'http://www.example.com', local: true) do %>Hello world!<% end %>") + + expected = whole_form("http://www.example.com", local: true) do + "Hello world!" + end + + assert_dom_equal expected, output_buffer + end +end + +class FormWithActsLikeFormForTest < FormWithTest + def form_with(*) + @output_buffer = super + end + + teardown do + I18n.backend.reload! + end + + setup do + # Create "label" locale for testing I18n label helpers + I18n.backend.store_translations "label", + activemodel: { + attributes: { + post: { + cost: "Total cost" + }, + "post/language": { + spanish: "Espanol" + } + } + }, + helpers: { + label: { + post: { + body: "Write entire text here", + color: { + red: "Rojo" + }, + comments: { + body: "Write body here" + } + }, + tag: { + value: "Tag" + }, + post_delegate: { + title: "Delegate model_name title" + } + } + } + + # Create "submit" locale for testing I18n submit helpers + I18n.backend.store_translations "submit", + helpers: { + submit: { + create: "Create %{model}", + update: "Confirm %{model} changes", + submit: "Save changes", + another_post: { + update: "Update your %{model}" + } + } + } + + I18n.backend.store_translations "placeholder", + activemodel: { + attributes: { + post: { + cost: "Total cost" + }, + "post/cost": { + uk: "Pounds" + } + } + }, + helpers: { + placeholder: { + post: { + title: "What is this about?", + written_on: { + spanish: "Escrito en" + }, + comments: { + body: "Write body here" + } + }, + post_delegate: { + title: "Delegate model_name title" + }, + tag: { + value: "Tag" + } + } + } + + @post = Post.new + @comment = Comment.new + def @post.errors() + Class.new { + def [](field); field == "author_name" ? ["can't be empty"] : [] end + def empty?() false end + def count() 1 end + def full_messages() ["Author name can't be empty"] end + }.new + end + def @post.to_key; [123]; end + def @post.id; 0; end + def @post.id_before_type_cast; "omg"; end + def @post.id_came_from_user?; true; end + def @post.to_param; "123"; end + + @post.persisted = true + @post.title = "Hello World" + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.secret = 1 + @post.written_on = Date.new(2004, 6, 15) + + @post.comments = [] + @post.comments << @comment + + @post.tags = [] + @post.tags << Tag.new + + @post_delegator = PostDelegator.new + + @post_delegator.title = "Hello World" + + @car = Car.new("#000FFF") + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + resources :posts do + resources :comments + end + + namespace :admin do + resources :posts do + resources :comments + end + end + + get "/foo", to: "controller#action" + root to: "main#index" + end + + def _routes + Routes + end + + include Routes.url_helpers + + def url_for(object) + @url_for_options = object + + if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank? + object.merge!(controller: "main", action: "index") + end + + super + end + + def test_form_with_requires_arguments + error = assert_raises(ArgumentError) do + form_for(nil, html: { id: "create-post" }) do + end + end + assert_equal "First argument in form cannot contain nil or be empty", error.message + + error = assert_raises(ArgumentError) do + form_for([nil, nil], html: { id: "create-post" }) do + end + end + assert_equal "First argument in form cannot contain nil or be empty", error.message + end + + def test_form_with + 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") + concat f.button("Create post") + concat f.button { + concat content_tag(:span, "Create post") + } + end + + 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' />" \ + "<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>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_only_url_on_create + form_with(url: "/posts") do |f| + concat f.label :title, "Label me" + concat f.text_field :title + end + + expected = whole_form("/posts") do + '<label for="title">Label me</label>' \ + '<input type="text" name="title">' + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_only_url_on_update + form_with(url: "/posts/123") do |f| + concat f.label :title, "Label me" + concat f.text_field :title + end + + expected = whole_form("/posts/123") do + '<label for="title">Label me</label>' \ + '<input type="text" name="title">' + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_general_attributes + form_with(url: "/posts/123") do |f| + concat f.text_field :no_model_to_back_this_badboy + end + + expected = whole_form("/posts/123") do + '<input type="text" name="no_model_to_back_this_badboy">' + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_attribute_not_on_model + form_with(model: @post) do |f| + concat f.text_field :this_dont_exist_on_post + end + + expected = whole_form("/posts/123", method: :patch) do + '<input type="text" name="post[this_dont_exist_on_post]">' + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_doesnt_call_private_or_protected_properties_on_form_object_skipping_value + obj = Class.new do + private def private_property + "That would be great." + end + + protected def protected_property + "I believe you have my stapler." + end + end.new + + form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f| + 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) + end + end + + def test_form_with_with_collection_radio_buttons + post = Post.new + def post.active; false; end + form_with(model: post) do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts") do + "<input type='hidden' name='post[active]' value='' />" \ + "<input name='post[active]' type='radio' value='true' />" \ + "<label for='post_active_true'>true</label>" \ + "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<label for='post_active_false'>false</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_radio_buttons_with_custom_builder_block + post = Post.new + def post.active; false; end + + form_with(model: post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + end + + expected = whole_form("/posts") do + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "false</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template + post = Post.new + def post.active; false; end + def post.id; 1; end + + form_with(model: post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + concat f.hidden_field :id + end + + expected = whole_form("/posts") do + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "false</label>" \ + "<input name='post[id]' type='hidden' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_index_and_with_collection_radio_buttons + post = Post.new + def post.active; false; end + + form_with(model: post, index: "1") do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts") do + "<input type='hidden' name='post[1][active]' value='' />" \ + "<input name='post[1][active]' type='radio' value='true' />" \ + "<label for='post_1_active_true'>true</label>" \ + "<input checked='checked' name='post[1][active]' type='radio' value='false' />" \ + "<label for='post_1_active_false'>false</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + form_with(model: post) do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + expected = whole_form("/posts") do + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<label for='post_tag_ids_1'>Tag 1</label>" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<label for='post_tag_ids_2'>Tag 2</label>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<label for='post_tag_ids_3'>Tag 3</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_check_boxes_with_custom_builder_block + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + form_with(model: post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + end + + expected = whole_form("/posts") do + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "Tag 3</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template + post = Post.new + def post.tag_ids; [1, 3]; end + def post.id; 1; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + + form_with(model: post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + concat f.hidden_field :id + end + + expected = whole_form("/posts") do + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "Tag 3</label>" \ + "<input name='post[id]' type='hidden' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_index_and_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1]; end + collection = [[1, "Tag 1"]] + + form_with(model: post, index: "1") do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + expected = whole_form("/posts") do + "<input name='post[1][tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' />" \ + "<label for='post_1_tag_ids_1'>Tag 1</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_file_field_generate_multipart + form_with(model: @post, id: "create-post") do |f| + concat f.file_field(:file) + end + + expected = whole_form("/posts/123", "create-post", method: "patch", multipart: true) do + "<input name='post[file]' type='file' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_file_field_generate_multipart + form_with(model: @post) do |f| + concat f.fields(:comment, model: @post) { |c| + concat c.file_field(:file) + } + end + + expected = whole_form("/posts/123", method: "patch", multipart: true) do + "<input name='post[comment][file]' type='file' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_format + form_with(model: @post, format: :json, id: "edit_post_123", class: "edit_post") do |f| + concat f.label(:title) + end + + expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: "patch") do + "<label for='post_title'>Title</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_format_and_url + form_with(model: @post, format: :json, url: "/") do |f| + concat f.label(:title) + end + + expected = whole_form("/", method: "patch") do + "<label for='post_title'>Title</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_model_using_relative_model_naming + blog_post = Blog::Post.new("And his name will be forty and four.", 44) + + form_with(model: blog_post) do |f| + concat f.text_field :title + concat f.submit("Edit post") + 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='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_symbol_scope + form_with(model: @post, scope: "other_name", id: "create-post") do |f| + concat f.label(:title, class: "post_title") + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit("Create post") + end + + 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[secret]' value='0' type='hidden' />" \ + "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' />" \ + "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_method_as_part_of_html_options + form_with(model: @post, url: "/", id: "create-post", html: { method: :delete }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_method + form_with(model: @post, url: "/", method: :delete, id: "create-post") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_search_field + # Test case for bug which would emit an "object" attribute + # when used with form_for using a search_field form helper + form_with(model: Post.new, url: "/search", id: "search-post", method: :get) do |f| + concat f.search_field(:title) + end + + expected = whole_form("/search", "search-post", method: "get") do + "<input name='post[title]' type='search' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_enables_remote_by_default + form_with(model: @post, url: "/", id: "create-post", method: :patch) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_is_not_remote_by_default_if_form_with_generates_remote_forms_is_false + old_value = ActionView::Helpers::FormHelper.form_with_generates_remote_forms + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = false + + form_with(model: @post, url: "/", id: "create-post", method: :patch) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = old_value + end + + def test_form_with_skip_enforcing_utf8_true + form_with(scope: :post, skip_enforcing_utf8: true) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", skip_enforcing_utf8: true) do + "<input name='post[title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_skip_enforcing_utf8_false + form_with(scope: :post, skip_enforcing_utf8: false) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", skip_enforcing_utf8: false) do + "<input name='post[title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_without_object + form_with(scope: :post, id: "create-post") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_index + form_with(model: @post, scope: "post[]") do |f| + concat f.label(:title) + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + 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][secret]' type='hidden' value='0' />" \ + "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_nil_index_option_override + form_with(model: @post, scope: "post[]", index: nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[][secret]' type='hidden' value='0' />" \ + "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_label_error_wrapping + form_with(model: @post) do |f| + concat f.label(:author_name, class: "label") + concat f.text_field(:author_name) + concat f.submit("Create post") + end + + 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>" \ + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_label_error_wrapping_without_conventional_instance_variable + post = remove_instance_variable :@post + + form_with(model: post) do |f| + concat f.label(:author_name, class: "label") + concat f.text_field(:author_name) + concat f.submit("Create post") + end + + 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>" \ + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_label_error_wrapping_block_and_non_block_versions + form_with(model: @post) do |f| + concat f.label(:author_name, "Name", class: "label") + concat f.label(:author_name, class: "label") { "Name" } + end + + expected = whole_form("/posts/123", method: "patch") do + "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" \ + "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_namespace + skip "Do namespaces still make sense?" + form_for(@post, namespace: "namespace") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_submit_with_object_as_new_record_and_locale_strings + with_locale :submit do + @post.persisted = false + @post.stub(:to_key, nil) do + form_with(model: @post) do |f| + concat f.submit + end + + expected = whole_form("/posts") do + "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />" + end + + assert_dom_equal expected, output_buffer + end + end + end + + def test_submit_with_object_as_existing_record_and_locale_strings + with_locale :submit do + form_with(model: @post) do |f| + concat f.submit + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_submit_without_object_and_locale_strings + with_locale :submit do + form_with(scope: :post) do |f| + concat f.submit class: "extra" + end + + expected = whole_form do + "<input name='commit' class='extra' data-disable-with='Save changes' type='submit' value='Save changes' />" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_submit_with_object_and_nested_lookup + with_locale :submit do + form_with(model: @post, scope: :another_post) do |f| + concat f.submit + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_fields_with_attributes_not_on_model + form_with(model: @post) do |f| + concat f.fields(:comment) { |c| + concat c.text_field :dont_exist_on_model + } + end + + expected = whole_form("/posts/123", method: :patch) do + '<input type="text" name="post[comment][dont_exist_on_model]">' + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_attributes_not_on_model_deep_nested + @comment.save + form_with(scope: :posts) do |f| + f.fields("post[]", model: @post) do |f2| + f2.text_field(:id) + @post.comments.each do |comment| + concat f2.fields("comment[]", model: comment) { |c| + concat c.text_field(:dont_exist_on_model) + } + end + end + end + + expected = whole_form do + '<input name="posts[post][0][comment][1][dont_exist_on_model]" type="text">' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields + @comment.body = "Hello World" + form_with(model: @post) do |f| + concat f.fields(model: @comment) { |c| + concat c.text_field(:body) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[comment][body]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_deep_nested_fields + @comment.save + form_with(scope: :posts) do |f| + f.fields("post[]", model: @post) do |f2| + f2.text_field(:id) + @post.comments.each do |comment| + concat f2.fields("comment[]", model: comment) { |c| + concat c.text_field(:name) + } + end + end + end + + expected = whole_form do + "<input name='posts[post][0][comment][1][name]' type='text' value='comment #1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_nested_collections + form_with(model: @post, scope: "post[]") do |f| + concat f.text_field(:title) + concat f.fields("comment[]", model: @comment) { |c| + concat c.text_field(:name) + } + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_index_and_parent_fields + form_with(model: @post, index: 1) do |c| + concat c.text_field(:title) + concat c.fields("comment", model: @comment, index: 1) { |r| + concat r.text_field(:name) + } + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_index_and_nested_fields + output_buffer = form_with(model: @post, index: 1) do |f| + concat f.fields(:comment, model: @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[1][comment][title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_index_on_both + form_with(model: @post, index: 1) do |f| + concat f.fields(:comment, model: @post, index: 5) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[1][comment][5][title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_auto_index + form_with(model: @post, scope: "post[]") do |f| + concat f.fields(:comment, model: @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[123][comment][title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_index_radio_button + form_with(model: @post) do |f| + concat f.fields(:comment, model: @post, index: 5) { |c| + concat c.radio_button(:title, "hello") + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[comment][5][title]' type='radio' value='hello' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_auto_index_on_both + form_with(model: @post, scope: "post[]") do |f| + concat f.fields("comment[]", model: @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[123][comment][123][title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_index_and_auto_index + output_buffer = form_with(model: @post, scope: "post[]") do |f| + concat f.fields(:comment, model: @post, index: 5) { |c| + concat c.text_field(:title) + } + end + + output_buffer << form_with(model: @post, index: 1) do |f| + concat f.fields("comment[]", model: @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[123][comment][5][title]' type='text' value='Hello World' />" + end + whole_form("/posts/123", method: "patch") do + "<input name='post[1][comment][123][title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_a_new_record_on_a_nested_attributes_one_to_one_association + @post.author = Author.new + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + concat af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association + form_with(model: @post) do |f| + f.fields(:author, model: Author.new(123)) do |af| + assert_not_nil af.object + assert_equal 123, af.object.id + end + end + end + + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + concat af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:author, skip_id: true) { |af| + af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited + @post.author = Author.new(321) + + form_with(model: @post, skip_id: true) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override + @post.author = Author.new(321) + + form_with(model: @post, skip_id: true) do |f| + concat f.text_field(:title) + concat f.fields(:author, skip_id: false) { |af| + af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + concat af.hidden_field(:id) + concat af.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields(:comments, model: comment, skip_id: true) { |cf| + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_with(model: @post, skip_id: true) do |f| + concat f.text_field(:title) + concat f.fields(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_with(model: @post, skip_id: true) do |f| + concat f.text_field(:title) + concat f.fields(:author, skip_id: false) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + concat cf.hidden_field(:id) + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new, Comment.new] + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_and_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new(321), Comment.new] + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields(:comments, model: comment) { |cf| + concat cf.text_field(:name) + } + end + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_an_empty_supplied_attributes_collection + form_with(model: @post) do |f| + concat f.text_field(:title) + f.fields(:comments, model: []) do |cf| + concat cf.text_field(:name) + end + end + + expected = whole_form("/posts/123", method: "patch") do + '<input name="post[title]" type="text" value="Hello World" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_existing_records_on_a_supplied_nested_attributes_collection + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:comments, model: @post.comments) { |cf| + concat cf.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_arel_like + @post.comments = ArelLike.new + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:comments, model: @post.comments) { |cf| + concat cf.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_label_translation_with_more_than_10_records + @post.comments = Array.new(11) { |id| Comment.new(id + 1) } + + params = 11.times.map { ["post.comments.body", default: [:"comment.body", ""], scope: "helpers.label"] } + assert_called_with(I18n, :t, params, returns: "Write body here") do + form_with(model: @post) do |f| + f.fields(:comments) do |cf| + concat cf.label(:body) + end + end + end + end + + def test_nested_fields_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one + comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.comments = [] + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:comments, model: comments) { |cf| + concat cf.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_on_a_nested_attributes_collection_association_yields_only_builder + @post.comments = [Comment.new(321), Comment.new] + yielded_comments = [] + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields(:comments) { |cf| + concat cf.text_field(:name) + yielded_comments << cf.object + } + 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" />' + end + + assert_dom_equal expected, output_buffer + assert_equal yielded_comments, @post.comments + end + + def test_nested_fields_with_child_index_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_with(model: @post) do |f| + concat f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf| + concat cf.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_with(model: @post) do |f| + concat f.fields(:comments, model: Comment.new(321), child_index: -> { "abc" }) { |cf| + concat cf.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + class FakeAssociationProxy + def to_ary + [1, 2, 3] + end + end + + def test_nested_fields_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy + @post.comments = FakeAssociationProxy.new + + form_with(model: @post) do |f| + concat f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf| + concat cf.text_field(:name) + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_index_method_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + expected = 0 + @post.comments.each do |comment| + f.fields(:comments, model: comment) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + end + + def test_nested_fields_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new(321), Comment.new] + + form_with(model: @post) do |f| + expected = 0 + @post.comments.each do |comment| + f.fields(:comments, model: comment) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + end + + def test_nested_fields_index_method_with_existing_records_on_a_supplied_nested_attributes_collection + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + expected = 0 + f.fields(:comments, model: @post.comments) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + + def test_nested_fields_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_with(model: @post) do |f| + f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf| + assert_equal cf.index, "abc" + } + end + end + + def test_nested_fields_uses_unique_indices_for_different_collection_associations + @post.comments = [Comment.new(321)] + @post.tags = [Tag.new(123), Tag.new(456)] + @post.comments[0].relevances = [] + @post.tags[0].relevances = [] + @post.tags[1].relevances = [] + + form_with(model: @post) do |f| + concat f.fields(:comments, model: @post.comments[0]) { |cf| + concat cf.text_field(:name) + concat cf.fields(:relevances, model: CommentRelevance.new(314)) { |crf| + concat crf.text_field(:value) + } + } + concat f.fields(:tags, model: @post.tags[0]) { |tf| + concat tf.text_field(:value) + concat tf.fields(:relevances, model: TagRelevance.new(3141)) { |trf| + concat trf.text_field(:value) + } + } + concat f.fields("tags", model: @post.tags[1]) { |tf| + concat tf.text_field(:value) + concat tf.fields(:relevances, model: TagRelevance.new(31415)) { |trf| + concat trf.text_field(:value) + } + } + 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" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_with_hash_like_model + @author = HashBackedAuthor.new + + form_with(model: @post) do |f| + concat f.fields(:author, model: @author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '<input name="post[author_attributes][name]" type="text" value="hash backed author" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_fields + output_buffer = fields(:post, model: @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_index + output_buffer = fields("post[]", model: @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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][secret]' type='hidden' value='0' />" \ + "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_nil_index_option_override + output_buffer = fields("post[]", model: @post, index: nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[][secret]' type='hidden' value='0' />" \ + "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_index_option_override + output_buffer = fields("post[]", model: @post, index: "abc") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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][secret]' type='hidden' value='0' />" \ + "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_without_object + output_buffer = fields(:post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_only_object + output_buffer = fields(model: @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_object_with_bracketed_name + output_buffer = fields("author[post]", model: @post) do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + assert_dom_equal "<label for=\"author_post_title\">Title</label>" \ + "<input name='author[post][title]' type='text' value='Hello World' />", + output_buffer + end + + def test_fields_object_with_bracketed_name_and_index + output_buffer = fields("author[post]", model: @post, index: 1) do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \ + "<input name='author[post][1][title]' type='text' value='Hello World' />", + output_buffer + end + + def test_form_builder_does_not_have_form_with_method + assert_not_includes ActionView::Helpers::FormBuilder.instance_methods, :form_with + end + + def test_form_with_and_fields + form_with(model: @post, scope: :post, id: "create-post") do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) + + concat fields(:parent_post, model: @post) { |parent_fields| + concat parent_fields.check_box(:secret) + } + 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='parent_post[secret]' type='hidden' value='0' />" \ + "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_and_fields_with_object + form_with(model: @post, scope: :post, id: "create-post") do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) + + concat post_form.fields(model: @comment) { |comment_fields| + concat comment_fields.text_field(:name) + } + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_and_fields_with_non_nested_association_and_without_object + form_with(model: @post) do |f| + concat f.fields(:category) { |c| + concat c.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "<input name='post[category][name]' type='text' />" + end + + assert_dom_equal expected, output_buffer + end + + class LabelledFormBuilder < ActionView::Helpers::FormBuilder + (field_helpers - %w(hidden_field)).each do |selector| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(field, *args, &proc) + ("<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>").html_safe + end + RUBY_EVAL + end + end + + def test_form_with_with_labelled_builder + form_with(model: @post, builder: LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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/>" + end + + assert_dom_equal expected, output_buffer + end + + def test_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, LabelledFormBuilder + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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/>" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + + def test_lazy_loading_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, "FormWithActsLikeFormForTest::LabelledFormBuilder" + + form_with(model: @post) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/posts/123", method: "patch") do + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + + def test_form_builder_override + self.default_form_builder = LabelledFormBuilder + + output_buffer = fields(:post, model: @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + + def test_lazy_loading_form_builder_override + self.default_form_builder = "FormWithActsLikeFormForTest::LabelledFormBuilder" + + output_buffer = fields(:post, model: @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + + def test_fields_with_labelled_builder + output_buffer = fields(:post, model: @post, builder: LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + 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/>" + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_labelled_builder_with_nested_fields_without_options_hash + klass = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + f.fields(:comments, model: Comment.new) do |nested_fields| + klass = nested_fields.class + "" + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_with_with_labelled_builder_with_nested_fields_with_options_hash + klass = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + f.fields(:comments, model: Comment.new, index: "foo") do |nested_fields| + klass = nested_fields.class + "" + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_with_with_labelled_builder_path + path = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + path = f.to_partial_path + "" + end + + assert_equal "labelled_form", path + end + + class LabelledFormBuilderSubclass < LabelledFormBuilder; end + + def test_form_with_with_labelled_builder_with_nested_fields_with_custom_builder + klass = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + f.fields(:comments, model: Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields| + klass = nested_fields.class + "" + end + end + + assert_equal LabelledFormBuilderSubclass, klass + end + + def test_form_with_with_html_options_adds_options_to_form_tag + form_with(model: @post, html: { id: "some_form", class: "some_class", multipart: true }) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data") + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_string_url_option + form_with(model: @post, url: "http://www.otherdomain.com") do |f| end + + assert_dom_equal whole_form("http://www.otherdomain.com", method: "patch"), output_buffer + end + + def test_form_with_with_hash_url_option + form_with(model: @post, url: { controller: "controller", action: "action" }) do |f| end + + assert_equal "controller", @url_for_options[:controller] + assert_equal "action", @url_for_options[:action] + end + + def test_form_with_with_record_url_option + form_with(model: @post, url: @post) do |f| end + + expected = whole_form("/posts/123", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object + form_with(model: @post) do |f| end + + expected = whole_form("/posts/123", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_new_object + post = Post.new + post.persisted = false + def post.to_key; nil; end + + form_with(model: post) {} + + expected = whole_form("/posts") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object_in_list + @comment.save + form_with(model: [@post, @comment]) {} + + expected = whole_form(post_comment_path(@post, @comment), method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_new_object_in_list + form_with(model: [@post, @comment]) {} + + expected = whole_form(post_comments_path(@post)) + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object_and_namespace_in_list + @comment.save + form_with(model: [:admin, @post, @comment]) {} + + expected = whole_form(admin_post_comment_path(@post, @comment), method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_new_object_and_namespace_in_list + form_with(model: [:admin, @post, @comment]) {} + + expected = whole_form(admin_post_comments_path(@post)) + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object_and_custom_url + form_with(model: @post, url: "/super_posts") do |f| end + + expected = whole_form("/super_posts", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_default_method_as_patch + form_with(model: @post) {} + expected = whole_form("/posts/123", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_data_attributes + form_with(model: @post, data: { behavior: "stuff" }) {} + assert_match %r|data-behavior="stuff"|, output_buffer + assert_match %r|data-remote="true"|, output_buffer + end + + def test_fields_returns_block_result + output = fields(model: Post.new) { |f| "fields" } + assert_equal "fields", output + end + + def test_form_with_only_instantiates_builder_once + initialization_count = 0 + builder_class = Class.new(ActionView::Helpers::FormBuilder) do + define_method :initialize do |*args| + super(*args) + initialization_count += 1 + end + end + + form_with(model: @post, builder: builder_class) {} + assert_equal 1, initialization_count, "form builder instantiated more than once" + end + + private + def hidden_fields(options = {}) + method = options[:method] + + if options.fetch(:skip_enforcing_utf8, false) + txt = "" + else + txt = %{<input name="utf8" type="hidden" value="✓" />} + end + + if method && !%w(get post).include?(method.to_s) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end + + txt + end + + def form_text(action = "/", id = nil, html_class = nil, local = nil, multipart = nil, method = nil) + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if multipart + txt << %{ data-remote="true"} unless local + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + method = method.to_s == "get" ? "get" : "post" + txt << %{ method="#{method}">} + end + + def whole_form(action = "/", id = nil, html_class = nil, local: false, **options) + contents = block_given? ? yield : "" + + method, multipart = options.values_at(:method, :multipart) + + form_text(action, id, html_class, local, multipart, method) + hidden_fields(options.slice :method, :skip_enforcing_utf8) + contents + "</form>" + end + + def protect_against_forgery? + false + end + + def with_locale(testing_locale = :label) + old_locale, I18n.locale = I18n.locale, testing_locale + yield + ensure + I18n.locale = old_locale + end +end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 395c4e4f41..b3a180b28a 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -257,11 +257,11 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_non_active_record_object - form_for(OpenStruct.new(name:"ok"), as: "person", url: "an_url", html: { id: "create-person" }) do |f| + form_for(OpenStruct.new(name: "ok"), as: "person", url: "/an", html: { id: "create-person" }) do |f| f.label(:name) end - expected = whole_form("an_url", "create-person", "new_person", method: "post") do + expected = whole_form("/an", "create-person", "new_person", method: "post") do '<label for="person_name">Name</label>' end @@ -600,7 +600,7 @@ class FormHelperTest < ActionView::TestCase def test_check_box_checked_if_option_checked_is_present assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', - check_box("post", "secret", "checked"=>"checked") + check_box("post", "secret", "checked" => "checked") ) end @@ -746,7 +746,7 @@ class FormHelperTest < ActionView::TestCase end def test_check_box_with_multiple_behavior - @post.comment_ids = [2,3] + @post.comment_ids = [2, 3] assert_dom_equal( '<input name="post[comment_ids][]" type="hidden" value="0" /><input id="post_comment_ids_1" name="post[comment_ids][]" type="checkbox" value="1" />', check_box("post", "comment_ids", { multiple: true }, 1) @@ -758,7 +758,7 @@ class FormHelperTest < ActionView::TestCase end def test_check_box_with_multiple_behavior_and_index - @post.comment_ids = [2,3] + @post.comment_ids = [2, 3] assert_dom_equal( '<input name="post[foo][comment_ids][]" type="hidden" value="0" /><input id="post_foo_comment_ids_1" name="post[foo][comment_ids][]" type="checkbox" value="1" />', check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1) @@ -767,7 +767,6 @@ class FormHelperTest < ActionView::TestCase '<input name="post[bar][comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_bar_comment_ids_3" name="post[bar][comment_ids][]" type="checkbox" value="3" />', check_box("post", "comment_ids", { multiple: true, index: "bar" }, 3) ) - end def test_checkbox_disabled_disables_hidden_field @@ -1033,7 +1032,7 @@ class FormHelperTest < ActionView::TestCase def test_date_field_with_value_attr expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2013-06-29" />} - value = Date.new(2013,6,29) + value = Date.new(2013, 6, 29) assert_dom_equal(expected, date_field("post", "written_on", value: value)) end @@ -1141,7 +1140,7 @@ class FormHelperTest < ActionView::TestCase def test_datetime_field_with_value_attr expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2013-06-29T13:37:00+00:00" />} - value = DateTime.new(2013,6,29,13,37) + value = DateTime.new(2013, 6, 29, 13, 37) assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) end @@ -1436,7 +1435,7 @@ class FormHelperTest < ActionView::TestCase ) assert_dom_equal( %{<input id="post_#{pid}_title" name="post[#{pid}][title]" type="text" value="Hello World" />}, - text_field("post[]","title") + text_field("post[]", "title") ) assert_dom_equal( %{<textarea id="post_#{pid}_body" name="post[#{pid}][body]">\nBack to the hill and over it again!</textarea>}, @@ -1515,13 +1514,13 @@ class FormHelperTest < ActionView::TestCase 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>" + + "<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 @@ -1536,10 +1535,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + - "<label for='post_active_true'>true</label>" + - "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \ + "<label for='post_active_true'>true</label>" \ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \ "<label for='post_active_false'>false</label>" end @@ -1558,12 +1557,12 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<label for='post_active_true'>"+ - "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + - "true</label>" + - "<label for='post_active_false'>"+ - "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \ "false</label>" end @@ -1584,13 +1583,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<label for='post_active_true'>"+ - "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + - "true</label>" + - "<label for='post_active_false'>"+ - "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + - "false</label>"+ + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \ + "false</label>" \ "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1606,10 +1605,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" + - "<label for='foo_post_active_true'>true</label>" + - "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" \ + "<label for='foo_post_active_true'>true</label>" \ + "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" \ "<label for='foo_post_active_false'>false</label>" end @@ -1625,10 +1624,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input type='hidden' name='post[1][active]' value='' />" + - "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" + - "<label for='post_1_active_true'>true</label>" + - "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" + + "<input type='hidden' name='post[1][active]' value='' />" \ + "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" \ + "<label for='post_1_active_true'>true</label>" \ + "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" \ "<label for='post_1_active_false'>false</label>" end @@ -1644,12 +1643,12 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "<label for='post_tag_ids_1'>Tag 1</label>" + - "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + - "<label for='post_tag_ids_2'>Tag 2</label>" + - "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<label for='post_tag_ids_1'>Tag 1</label>" \ + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<label for='post_tag_ids_2'>Tag 2</label>" \ + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \ "<label for='post_tag_ids_3'>Tag 3</label>" end @@ -1668,15 +1667,15 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<label for='post_tag_ids_1'>" + - "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "Tag 1</label>" + - "<label for='post_tag_ids_2'>" + - "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + - "Tag 2</label>" + - "<label for='post_tag_ids_3'>" + - "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \ "Tag 3</label>" end @@ -1698,16 +1697,16 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />"+ - "<label for='post_tag_ids_1'>" + - "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "Tag 1</label>" + - "<label for='post_tag_ids_2'>" + - "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + - "Tag 2</label>" + - "<label for='post_tag_ids_3'>" + - "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + - "Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "Tag 3</label>" \ "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1724,8 +1723,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ "<label for='foo_post_tag_ids_1'>Tag 1</label>" end @@ -1742,8 +1741,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input name='post[1][tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" + + "<input name='post[1][tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" \ "<label for='post_1_tag_ids_1'>Tag 1</label>" end @@ -1751,8 +1750,6 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_file_field_generate_multipart - Post.send :attr_accessor, :file - form_for(@post, html: { id: "create-post" }) do |f| concat f.file_field(:file) end @@ -1765,8 +1762,6 @@ class FormHelperTest < ActionView::TestCase end def test_fields_for_with_file_field_generate_multipart - Comment.send :attr_accessor, :file - form_for(@post) do |f| concat f.fields_for(:comment, @post) { |c| concat c.file_field(:file) @@ -1801,7 +1796,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" + + "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" \ "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" end @@ -1817,12 +1812,12 @@ class FormHelperTest < ActionView::TestCase concat f.submit("Create post") end - expected = whole_form("/posts/123", "create-post", "edit_other_name", method: "patch") do - "<label for='other_name_title' class='post_title'>Title</label>" + - "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" + - "<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' id='other_name_secret' value='1' type='checkbox' />" + + expected = whole_form("/posts/123", "create-post", "edit_other_name", method: "patch") do + "<label for='other_name_title' class='post_title'>Title</label>" \ + "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" \ + "<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' id='other_name_secret' value='1' type='checkbox' />" \ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" end @@ -1833,9 +1828,9 @@ class FormHelperTest < ActionView::TestCase obj = Class.new do private - def private_property - raise "This method should not be called." - end + def private_property + raise "This method should not be called." + end end.new form_for(obj, as: "other_name", url: "/", html: { id: "edit-other-name" }) do |f| @@ -1850,10 +1845,10 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("/", "create-post", "edit_post", method: "delete") do - "<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' />" + + expected = whole_form("/", "create-post", "edit_post", method: "delete") do + "<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' />" end @@ -1867,10 +1862,10 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("/", "create-post", "edit_post", method: "delete") do - "<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' />" + + expected = whole_form("/", "create-post", "edit_post", method: "delete") do + "<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' />" end @@ -1884,7 +1879,7 @@ class FormHelperTest < ActionView::TestCase concat f.search_field(:title) end - expected = whole_form("/search", "search-post", "new_post", method: "get") do + expected = whole_form("/search", "search-post", "new_post", method: "get") do "<input name='post[title]' type='search' id='post_title' />" end @@ -1898,10 +1893,10 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do - "<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' />" + + expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do + "<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' />" end @@ -1939,10 +1934,10 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do - "<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' />" + + expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do + "<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' />" end @@ -1958,10 +1953,10 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("/posts", "new_post", "new_post", remote: true) do - "<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' />" + + expected = whole_form("/posts", "new_post", "new_post", remote: true) do + "<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' />" end @@ -1977,9 +1972,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/", "create-post") do - "<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[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' />" end @@ -1995,10 +1990,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do - "<label for='post_123_title'>Title</label>" + - "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + - "<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' />" + + "<label for='post_123_title'>Title</label>" \ + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \ + "<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' id='post_123_secret' value='1' />" end @@ -2013,9 +2008,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do - "<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[][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' />" end @@ -2030,8 +2025,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", 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' id='post_author_name' value='' /></div>" + + "<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' id='post_author_name' value='' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -2048,8 +2043,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", 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' id='post_author_name' value='' /></div>" + + "<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' id='post_author_name' value='' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -2063,7 +2058,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" \ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" end @@ -2078,9 +2073,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + - "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='namespace_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='namespace_post_secret' value='1' />" end @@ -2102,7 +2097,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<label for='namespace_post_title'>Title</label>" + + "<label for='namespace_post_title'>Title</label>" \ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" end @@ -2128,7 +2123,7 @@ class FormHelperTest < ActionView::TestCase end expected_1 = whole_form("/posts/123", "namespace_1_edit_post_123", "edit_post", method: "patch") do - "<label for='namespace_1_post_title'>Title</label>" + + "<label for='namespace_1_post_title'>Title</label>" \ "<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />" end @@ -2140,7 +2135,7 @@ class FormHelperTest < ActionView::TestCase end expected_2 = whole_form("/posts/123", "namespace_2_edit_post_123", "edit_post", method: "patch") do - "<label for='namespace_2_post_title'>Title</label>" + + "<label for='namespace_2_post_title'>Title</label>" \ "<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />" end @@ -2158,8 +2153,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + - "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />" end @@ -2266,11 +2261,13 @@ class FormHelperTest < ActionView::TestCase concat f.fields_for("comment[]", @comment) { |c| concat c.text_field(:name) } + concat f.text_field(:body) end expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do - "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + - "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />" + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \ + "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />" \ + "<input name='post[123][body]' type='text' id='post_123_body' value='Back to the hill and over it again!' />" end assert_dom_equal expected, output_buffer @@ -2285,7 +2282,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" + + "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" \ "<input name='post[1][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />" end @@ -2395,7 +2392,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="new author" />' end @@ -2422,8 +2419,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -2441,8 +2438,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -2460,7 +2457,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -2478,7 +2475,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -2496,8 +2493,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -2516,8 +2513,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -2537,10 +2534,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2564,10 +2561,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2591,9 +2588,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2617,10 +2614,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2640,10 +2637,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2664,10 +2661,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2687,8 +2684,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -2708,9 +2705,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -2743,10 +2740,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2764,10 +2761,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2799,10 +2796,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2822,9 +2819,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -2842,7 +2839,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -2853,13 +2850,13 @@ class FormHelperTest < ActionView::TestCase @post.comments = [] form_for(@post) do |f| - concat f.fields_for(:comments, Comment.new(321), child_index: -> { "abc" } ) { |cf| + concat f.fields_for(:comments, Comment.new(321), child_index: -> { "abc" }) { |cf| concat cf.text_field(:name) } end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -2882,7 +2879,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -2968,17 +2965,17 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + - '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + - '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' \ + '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' \ + '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' \ + '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' \ + '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' \ + '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' \ + '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' \ + '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' \ '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' end @@ -3009,9 +3006,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<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[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' />" assert_dom_equal expected, output_buffer @@ -3025,9 +3022,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + - "<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][title]' type='text' id='post_123_title' value='Hello World' />" \ + "<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' id='post_123_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3041,9 +3038,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<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[][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' />" assert_dom_equal expected, output_buffer @@ -3057,9 +3054,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" + - "<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][title]' type='text' id='post_abc_title' value='Hello World' />" \ + "<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' id='post_abc_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3073,9 +3070,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<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[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' />" assert_dom_equal expected, output_buffer @@ -3089,9 +3086,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<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[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' />" assert_dom_equal expected, output_buffer @@ -3103,7 +3100,7 @@ class FormHelperTest < ActionView::TestCase concat f.text_field(:title) end - assert_dom_equal "<label for=\"author_post_title\">Title</label>" + + assert_dom_equal "<label for=\"author_post_title\">Title</label>" \ "<input name='author[post][title]' type='text' id='author_post_title' value='Hello World' />", output_buffer end @@ -3114,7 +3111,7 @@ class FormHelperTest < ActionView::TestCase concat f.text_field(:title) end - assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" + + assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \ "<input name='author[post][1][title]' type='text' id='author_post_1_title' value='Hello World' />", output_buffer end @@ -3134,9 +3131,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do - "<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='parent_post[secret]' type='hidden' value='0' />" + + "<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='parent_post[secret]' type='hidden' value='0' />" \ "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />" end @@ -3154,8 +3151,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do - "<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[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[comment][name]' type='text' id='post_comment_name' value='new comment' />" end @@ -3194,8 +3191,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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' id='post_secret' value='1' /><br/>" end @@ -3213,8 +3210,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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' id='post_secret' value='1' /><br/>" end @@ -3272,8 +3269,8 @@ class FormHelperTest < ActionView::TestCase end expected = - "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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' id='post_secret' value='1' /><br/>" assert_dom_equal expected, output_buffer @@ -3443,7 +3440,7 @@ class FormHelperTest < ActionView::TestCase assert_equal 1, initialization_count, "form builder instantiated more than once" end - protected + private def hidden_fields(options = {}) method = options[:method] diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index 477d4f9eca..3247f20ba7 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -6,6 +6,15 @@ class Map < Hash end end +class CustomEnumerable + include Enumerable + + def each + yield "one" + yield "two" + end +end + class FormOptionsHelperTest < ActionView::TestCase tests ActionView::Helpers::FormOptionsHelper @@ -258,7 +267,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer - albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")] + albums = [ Album.new(1, "first", "rap"), Album.new(2, "second", "pop")] assert_dom_equal( %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>), options_from_collection_for_select(albums, "id", "genre", selected: "1") @@ -266,7 +275,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string - albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")] + albums = [ Album.new("1", "first", "rap"), Album.new("2", "second", "pop")] assert_dom_equal( %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>), @@ -275,7 +284,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_preselected_value_as_string_and_option_value_is_float - albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")] assert_dom_equal( %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>), @@ -284,7 +293,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_preselected_value_as_nil - albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")] assert_dom_equal( %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>), @@ -293,7 +302,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_disabled_value_as_nil - albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")] assert_dom_equal( %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>), @@ -302,7 +311,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_disabled_value_as_array - albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")] assert_dom_equal( %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>), @@ -311,11 +320,11 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float - albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ] + albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop"), Album.new(3.0, "third", "country") ] assert_dom_equal( %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>), - options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"]) + options_from_collection_for_select(albums, "id", "genre", ["1.0", "3.0"]) ) end @@ -335,9 +344,9 @@ class FormOptionsHelperTest < ActionView::TestCase "<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>", grouped_options_for_select([ ["North America", - [["United States","US"],"Canada"]], + [["United States", "US"], "Canada"]], ["Europe", - [["Great Britain","GB"], "Germany"]] + [["Great Britain", "GB"], "Germany"]] ]) ) end @@ -346,8 +355,8 @@ class FormOptionsHelperTest < ActionView::TestCase assert_dom_equal( "<optgroup label=\"North America\" data-foo=\"bar\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\" disabled=\"disabled\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>", grouped_options_for_select([ - ["North America", [["United States","US"],"Canada"], data: { foo: "bar" }], - ["Europe", [["Great Britain","GB"], "Germany"], disabled: "disabled"] + ["North America", [["United States", "US"], "Canada"], data: { foo: "bar" }], + ["Europe", [["Great Britain", "GB"], "Germany"], disabled: "disabled"] ]) ) end @@ -356,110 +365,110 @@ 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 def test_grouped_options_for_select_with_selected_and_prompt assert_dom_equal( "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", - grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...") + grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...") ) end def test_grouped_options_for_select_with_selected_and_prompt_true assert_dom_equal( "<option value=\"\">Please select</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", - grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: true) + grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]], "Cowboy Hat", prompt: true) ) end def test_grouped_options_for_select_returns_html_safe_string - assert grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]]).html_safe? + assert grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]]).html_safe? end def test_grouped_options_for_select_with_prompt_returns_html_escaped_string assert_dom_equal( "<option value=\"\"><Choose One></option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", - grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, prompt: "<Choose One>")) + grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]], nil, prompt: "<Choose One>")) end def test_optgroups_with_with_options_with_hash assert_dom_equal( "<optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup>", - grouped_options_for_select("North America" => ["United States","Canada"], "Europe" => ["Denmark","Germany"]) + grouped_options_for_select("North America" => ["United States", "Canada"], "Europe" => ["Denmark", "Germany"]) ) end def test_time_zone_options_no_params opts = time_zone_options_for_select - assert_dom_equal "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\">D</option>\n" + + assert_dom_equal "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\">D</option>\n" \ "<option value=\"E\">E</option>", opts end def test_time_zone_options_with_selected - opts = time_zone_options_for_select( "D" ) - assert_dom_equal "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + + opts = time_zone_options_for_select("D") + assert_dom_equal "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ "<option value=\"E\">E</option>", opts end def test_time_zone_options_with_unknown_selected - opts = time_zone_options_for_select( "K" ) - assert_dom_equal "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\">D</option>\n" + + opts = time_zone_options_for_select("K") + assert_dom_equal "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\">D</option>\n" \ "<option value=\"E\">E</option>", opts end def test_time_zone_options_with_priority_zones - zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] - opts = time_zone_options_for_select( nil, zones ) - assert_dom_equal "<option value=\"B\">B</option>\n" + - "<option value=\"E\">E</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"C\">C</option>\n" + + zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] + opts = time_zone_options_for_select(nil, zones) + assert_dom_equal "<option value=\"B\">B</option>\n" \ + "<option value=\"E\">E</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"C\">C</option>\n" \ "<option value=\"D\">D</option>", opts end def test_time_zone_options_with_selected_priority_zones - zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] - opts = time_zone_options_for_select( "E", zones ) - assert_dom_equal "<option value=\"B\">B</option>\n" + - "<option value=\"E\" selected=\"selected\">E</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"C\">C</option>\n" + + zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] + opts = time_zone_options_for_select("E", zones) + assert_dom_equal "<option value=\"B\">B</option>\n" \ + "<option value=\"E\" selected=\"selected\">E</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"C\">C</option>\n" \ "<option value=\"D\">D</option>", opts end def test_time_zone_options_with_unselected_priority_zones - zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] - opts = time_zone_options_for_select( "C", zones ) - assert_dom_equal "<option value=\"B\">B</option>\n" + - "<option value=\"E\">E</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"C\" selected=\"selected\">C</option>\n" + + zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] + opts = time_zone_options_for_select("C", zones) + assert_dom_equal "<option value=\"B\">B</option>\n" \ + "<option value=\"E\">E</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"C\" selected=\"selected\">C</option>\n" \ "<option value=\"D\">D</option>", opts end def test_time_zone_options_with_priority_zones_does_not_mutate_time_zones original_zones = ActiveSupport::TimeZone.all.dup - zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] + zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] time_zone_options_for_select(nil, zones) assert_equal original_zones, ActiveSupport::TimeZone.all end @@ -628,7 +637,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_select_with_multiple_to_add_hidden_input - output_buffer = select(:post, :category, "", {}, multiple: true) + output_buffer = select(:post, :category, "", {}, multiple: true) assert_dom_equal( "<input type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", output_buffer @@ -636,7 +645,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_select_with_multiple_and_without_hidden_input - output_buffer = select(:post, :category, "", { include_hidden: false }, multiple: true) + output_buffer = select(:post, :category, "", { include_hidden: false }, multiple: true) assert_dom_equal( "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", output_buffer @@ -644,7 +653,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_select_with_multiple_and_with_explicit_name_ending_with_brackets - output_buffer = select(:post, :category, [], { include_hidden: false }, multiple: true, name: "post[category][]") + output_buffer = select(:post, :category, [], { include_hidden: false }, multiple: true, name: "post[category][]") assert_dom_equal( "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", output_buffer @@ -652,7 +661,7 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input - output_buffer = select(:post, :category, "", {}, multiple: true, disabled: true) + output_buffer = select(:post, :category, "", {}, multiple: true, disabled: true) assert_dom_equal( "<input disabled=\"disabled\"type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" disabled=\"disabled\" id=\"post_category\" name=\"post[category][]\"></select>", output_buffer @@ -904,6 +913,14 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_enumerable + @post = Post.new + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option value=\"two\">two</option></select>", + select("post", "category", CustomEnumerable.new) + ) + end + def test_collection_select @post = Post.new @post.author_name = "Babe" @@ -1030,13 +1047,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select @firm = Firm.new("D") - html = time_zone_select( "firm", "time_zone" ) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + html = time_zone_select("firm", "time_zone") + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1049,12 +1066,12 @@ class FormOptionsHelperTest < ActionView::TestCase end assert_dom_equal( - "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", output_buffer ) @@ -1068,12 +1085,12 @@ class FormOptionsHelperTest < ActionView::TestCase end assert_dom_equal( - "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", output_buffer ) @@ -1088,12 +1105,12 @@ class FormOptionsHelperTest < ActionView::TestCase end assert_dom_equal( - "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", output_buffer ) @@ -1102,13 +1119,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_blank @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, include_blank: true) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"\"></option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"\"></option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1116,13 +1133,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_blank_as_string @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, include_blank: "No Zone") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"\">No Zone</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"\">No Zone</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1131,12 +1148,12 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, {}, "style" => "color: red") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {}, @@ -1147,13 +1164,13 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, { include_blank: true }, "style" => "color: red") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + - "<option value=\"\"></option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \ + "<option value=\"\"></option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, @@ -1164,13 +1181,13 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, { include_blank: "No Zone" }, "style" => "color: red") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + - "<option value=\"\">No Zone</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \ + "<option value=\"\">No Zone</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, @@ -1180,14 +1197,14 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_priority_zones @firm = Firm.new("D") zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ] - html = time_zone_select("firm", "time_zone", zones ) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"E\">E</option>" + + html = time_zone_select("firm", "time_zone", zones) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1200,13 +1217,13 @@ class FormOptionsHelperTest < ActionView::TestCase end html = time_zone_select("firm", "time_zone", /A|D/) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1221,13 +1238,13 @@ class FormOptionsHelperTest < ActionView::TestCase end html = time_zone_select("firm", "time_zone", /A|D/) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1236,13 +1253,13 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new() @firm.time_zone = nil - html = time_zone_select( "firm", "time_zone", nil, default: "B" ) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\" selected=\"selected\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\">D</option>\n" + - "<option value=\"E\">E</option>" + + html = time_zone_select("firm", "time_zone", nil, default: "B") + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\" selected=\"selected\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1250,13 +1267,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_default_time_zone_and_value @firm = Firm.new("D") - html = time_zone_select( "firm", "time_zone", nil, default: "B" ) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + html = time_zone_select("firm", "time_zone", nil, default: "B") + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index da929cac8f..084c540139 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -65,13 +65,13 @@ class FormTagHelperTest < ActionView::TestCase end def test_check_box_tag_disabled - actual = check_box_tag "admin","1", false, disabled: true + actual = check_box_tag "admin", "1", false, disabled: true expected = %(<input id="admin" disabled="disabled" name="admin" type="checkbox" value="1" />) assert_dom_equal expected, actual end def test_check_box_tag_default_checked - actual = check_box_tag "admin","1", true + actual = check_box_tag "admin", "1", true expected = %(<input id="admin" checked="checked" name="admin" type="checkbox" value="1" />) assert_dom_equal expected, actual end @@ -510,6 +510,13 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_submit_tag_doesnt_have_data_disable_with_twice_with_hash + assert_equal( + %(<input type="submit" name="commit" value="Save" data-disable-with="Processing..." />), + submit_tag("Save", data: { disable_with: "Processing..." }) + ) + end + def test_submit_tag_with_symbol_value assert_dom_equal( %(<input data-disable-with="Save" name='commit' type="submit" value="Save" />), @@ -694,31 +701,31 @@ class FormTagHelperTest < ActionView::TestCase def test_text_area_tag_options_symbolize_keys_side_effects options = { option: "random_option" } text_area_tag "body", "hello world", options - assert_equal options, option: "random_option" + assert_equal({ option: "random_option" }, options) end def test_submit_tag_options_symbolize_keys_side_effects options = { option: "random_option" } submit_tag "submit value", options - assert_equal options, option: "random_option" + assert_equal({ option: "random_option" }, options) end def test_button_tag_options_symbolize_keys_side_effects options = { option: "random_option" } button_tag "button value", options - assert_equal options, option: "random_option" + assert_equal({ option: "random_option" }, options) end def test_image_submit_tag_options_symbolize_keys_side_effects options = { option: "random_option" } image_submit_tag "submit source", options - assert_equal options, option: "random_option" + assert_equal({ option: "random_option" }, options) end def test_image_label_tag_options_symbolize_keys_side_effects options = { option: "random_option" } label_tag "submit source", "title", options - assert_equal options, option: "random_option" + assert_equal({ option: "random_option" }, options) end def protect_against_forgery? diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb index 0468c845d2..c7670b056b 100644 --- a/actionview/test/template/javascript_helper_test.rb +++ b/actionview/test/template/javascript_helper_test.rb @@ -7,7 +7,7 @@ class JavaScriptHelperTest < ActionView::TestCase setup do @old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json - ActiveSupport.escape_html_entities_in_json = true + ActiveSupport.escape_html_entities_in_json = true @template = self end @@ -18,7 +18,7 @@ class JavaScriptHelperTest < ActionView::TestCase def test_escape_javascript assert_equal "", escape_javascript(nil) assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos')) - assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) ) + assert_equal %(backslash\\\\test), escape_javascript(%(backslash\\test)) assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags)) assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding(Encoding::UTF_8).encode!) assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\251 newline).force_encoding(Encoding::UTF_8).encode!) @@ -47,8 +47,8 @@ class JavaScriptHelperTest < ActionView::TestCase # Setting the :extname option will control what extension (if any) is appended to the url for assets def test_javascript_include_tag assert_dom_equal "<script src='/foo.js'></script>", javascript_include_tag("/foo") - assert_dom_equal "<script src='/foo'></script>", javascript_include_tag("/foo", extname: false ) - assert_dom_equal "<script src='/foo.bar'></script>", javascript_include_tag("/foo", extname: ".bar" ) + assert_dom_equal "<script src='/foo'></script>", javascript_include_tag("/foo", extname: false) + assert_dom_equal "<script src='/foo.bar'></script>", javascript_include_tag("/foo", extname: ".bar") end def test_javascript_tag_with_options diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index ece059484c..584666d54b 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -39,7 +39,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def set_view_cache_dependencies def @view.view_cache_dependencies; []; end - def @view.fragment_cache_key(*); "ahoy `controller` dependency"; end + def @view.combined_fragment_cache_key(*); "ahoy `controller` dependency"; end end def test_render_file_template @@ -55,7 +55,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def test_render_text_template Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do - @view.render(text: "TEXT") + @view.render(plain: "TEXT") wait assert_equal 2, @logger.logged(:info).size diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index 2a2ada2b36..678120a9c9 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -4,7 +4,7 @@ class NumberHelperTest < ActionView::TestCase tests ActionView::Helpers::NumberHelper def test_number_to_phone - assert_equal nil, number_to_phone(nil) + assert_nil number_to_phone(nil) assert_equal "555-1234", number_to_phone(5551234) assert_equal "(800) 555-1212 x 123", number_to_phone(8005551212, area_code: true, extension: 123) assert_equal "+18005551212", number_to_phone(8005551212, country_code: 1, delimiter: "") @@ -13,7 +13,7 @@ class NumberHelperTest < ActionView::TestCase end def test_number_to_currency - assert_equal nil, number_to_currency(nil) + assert_nil number_to_currency(nil) assert_equal "$1,234,567,890.50", number_to_currency(1234567890.50) assert_equal "$1,234,567,892", number_to_currency(1234567891.50, precision: 0) assert_equal "1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", unit: raw("Kč"), format: "%n %u", negative_format: "%n - %u") @@ -25,7 +25,7 @@ class NumberHelperTest < ActionView::TestCase end def test_number_to_percentage - assert_equal nil, number_to_percentage(nil) + assert_nil number_to_percentage(nil) assert_equal "100.000%", number_to_percentage(100) assert_equal "100.000 %", number_to_percentage(100, format: "%n %") assert_equal "<b>100.000</b> %", number_to_percentage(100, format: "<b>%n</b> %") @@ -43,13 +43,13 @@ class NumberHelperTest < ActionView::TestCase end def test_number_with_delimiter - assert_equal nil, number_with_delimiter(nil) + assert_nil number_with_delimiter(nil) assert_equal "12,345,678", number_with_delimiter(12345678) assert_equal "0", number_with_delimiter(0) end def test_number_with_precision - assert_equal nil, number_with_precision(nil) + assert_nil number_with_precision(nil) assert_equal "-111.235", number_with_precision(-111.2346) assert_equal "111.00", number_with_precision(111, precision: 2) assert_equal "0.00100", number_with_precision(0.001, precision: 5) @@ -57,13 +57,13 @@ class NumberHelperTest < ActionView::TestCase end def test_number_to_human_size - assert_equal nil, number_to_human_size(nil) + assert_nil number_to_human_size(nil) assert_equal "3 Bytes", number_to_human_size(3.14159265) assert_equal "1.2 MB", number_to_human_size(1234567, precision: 2) end def test_number_to_human - assert_equal nil, number_to_human(nil) + assert_nil number_to_human(nil) assert_equal "0", number_to_human(0) assert_equal "1.23 Thousand", number_to_human(1234) assert_equal "489.0 Thousand", number_to_human(489000, precision: 4, strip_insignificant_zeros: false) diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb index 98b1938276..537b4393ee 100644 --- a/actionview/test/template/output_safety_helper_test.rb +++ b/actionview/test/template/output_safety_helper_test.rb @@ -26,13 +26,29 @@ class OutputSafetyHelperTest < ActionView::TestCase end test "safe_join should work recursively similarly to Array.join" do - joined = safe_join(["a",["b","c"]], ":") + joined = safe_join(["a", ["b", "c"]], ":") assert_equal "a:b:c", joined - joined = safe_join(['"a"',["<b>","<c>"]], " <br/> ") + joined = safe_join(['"a"', ["<b>", "<c>"]], " <br/> ") assert_equal ""a" <br/> <b> <br/> <c>", joined end + test "safe_join should return the safe string separated by $, when second argument is not passed" do + default_delimeter = $, + + begin + $, = nil + joined = safe_join(["a", "b"]) + assert_equal "ab", joined + + $, = "|" + joined = safe_join(["a", "b"]) + assert_equal "a|b", joined + ensure + $, = default_delimeter + end + end + test "to_sentence should escape non-html_safe values" do actual = to_sentence(%w(< > & ' ")) assert actual.html_safe? @@ -87,4 +103,15 @@ class OutputSafetyHelperTest < ActionView::TestCase assert_equal "one, two three", to_sentence(["one", "two", "three"], last_word_connector: " ") assert_equal "one, two and three", to_sentence(["one", "two", "three"], last_word_connector: " and ") end + + test "to_sentence is not affected by $," do + separator_was = $, + $, = "|" + begin + assert_equal "one and two", to_sentence(["one", "two"]) + assert_equal "one, two, and three", to_sentence(["one", "two", "three"]) + ensure + $, = separator_was + end + end end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index e7e0b147c7..9999607067 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -10,8 +10,8 @@ module RenderTestCases @view = Class.new(ActionView::Base) do def view_cache_dependencies; end - def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key, :views) + def combined_fragment_cache_key(key) + [ :views, key ] end end.new(paths, @assigns) @@ -27,7 +27,7 @@ module RenderTestCases def test_render_without_options e = assert_raises(ArgumentError) { @view.render() } - assert_match(/You invoked render but did not give any of (.+) option./, e.message) + assert_match(/You invoked render but did not give any of (.+) option\./, e.message) end def test_render_file @@ -83,6 +83,10 @@ module RenderTestCases assert_equal "<h1>Kein Kommentar</h1>", @view.render(template: "comments/empty", locale: [:de]) end + def test_render_template_with_variants + assert_equal "<h1>No Comment</h1>\n", @view.render(template: "comments/empty", variants: :grid) + end + def test_render_file_with_handlers assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: [:builder]) assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: :builder) @@ -134,7 +138,7 @@ module RenderTestCases end def test_render_file_with_full_path - template_path = File.join(File.dirname(__FILE__), "../fixtures/test/hello_world") + template_path = File.expand_path("../fixtures/test/hello_world", __dir__) assert_equal "Hello world!", @view.render(file: template_path) end @@ -156,7 +160,7 @@ module RenderTestCases end def test_render_outside_path - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) assert_raises ActionView::MissingTemplate do @view.render(template: "../\\../test/abstract_unit.rb") end @@ -170,6 +174,10 @@ module RenderTestCases assert_equal "partial html", @view.render(partial: "test/partial") end + def test_render_partial_with_variants + assert_equal "<h1>Partial with variants</h1>\n", @view.render(partial: "test/partial_with_variants", variants: :grid) + end + def test_render_partial_with_selected_format assert_equal "partial html", @view.render(partial: "test/partial", formats: :html) assert_equal "partial js", @view.render(partial: "test/partial", formats: [:js]) @@ -220,15 +228,15 @@ module RenderTestCases def test_render_partial_with_invalid_option_as e = assert_raises(ArgumentError) { @view.render(partial: "test/partial_only", as: "a-in") } - assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + + assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_hyphen_and_invalid_option_as e = assert_raises(ArgumentError) { @view.render(partial: "test/a-in", as: "a-in") } - assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + + assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ "and is followed by any combination of letters, numbers and underscores.", e.message end @@ -253,7 +261,7 @@ module RenderTestCases def test_render_sub_template_with_errors e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/sub_template_raise") } assert_match %r!method.*doesnt_exist!, e.message - assert_equal "Trace of template inclusion: #{File.expand_path("#{FIXTURE_LOAD_PATH}/test/sub_template_raise.html.erb")}", e.sub_template_message + assert_match %r{Trace of template inclusion: .*test/sub_template_raise\.html\.erb}, e.sub_template_message assert_equal "1", e.line_number assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end @@ -301,6 +309,15 @@ module RenderTestCases @view.render(partial: "test/local_inspector", collection: [ Customer.new("mary") ]) end + def test_render_partial_collection_with_different_partials_still_provides_partial_iteration + a = {} + b = {} + def a.to_partial_path; "test/partial_iteration_1"; end + def b.to_partial_path; "test/partial_iteration_2"; end + + assert_equal "local-variable\nlocal-variable", @controller_view.render([a, b]) + end + def test_render_partial_with_empty_collection_should_return_nil assert_nil @view.render(partial: "test/customer", collection: []) end @@ -393,13 +410,11 @@ module RenderTestCases assert_equal :partial_name_local_variable, exception.cause.name end - # TODO: The reason for this test is unclear, improve documentation - def test_render_partial_and_fallback_to_layout + def test_render_partial_with_no_block_given_to_yield assert_equal "Before (Josh)\n\nAfter", @view.render(partial: "test/layout_for_partial", locals: { name: "Josh" }) end - # TODO: The reason for this test is unclear, improve documentation - def test_render_missing_xml_partial_and_raise_missing_template + def test_render_partial_with_non_existent_format_and_raise_missing_template @view.formats = [:xml] assert_raises(ActionView::MissingTemplate) { @view.render(partial: "test/layout_for_partial") } ensure @@ -424,7 +439,7 @@ module RenderTestCases end CustomHandler = lambda do |template| - "@output_buffer = ''\n" + + "@output_buffer = ''\n" \ "@output_buffer << 'source: #{template.source.inspect}'\n" end @@ -475,11 +490,9 @@ module RenderTestCases end def test_render_ignores_templates_with_malformed_template_handlers - ActiveSupport::Deprecation.silence do - %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| - assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" - assert_raises(ActionView::MissingTemplate) { @view.render(file: "test/malformed/#{name}") } - end + %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| + assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" + assert_raises(ActionView::MissingTemplate) { @view.render(file: "test/malformed/#{name}") } end end @@ -705,6 +718,6 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase private def cache_key(*names, virtual_path) digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] - @view.fragment_cache_key([ *names, digest ]) + @view.combined_fragment_cache_key([ "#{virtual_path}:#{digest}", *names ]) end end diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb index 43e3f21076..8e21f4b828 100644 --- a/actionview/test/template/resolver_patterns_test.rb +++ b/actionview/test/template/resolver_patterns_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" class ResolverPatternsTest < ActiveSupport::TestCase def setup - path = File.expand_path("../../fixtures/", __FILE__) + path = File.expand_path("../fixtures", __dir__) pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}" @resolver = ActionView::FileSystemResolver.new(path, pattern) end diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index c8963fee9c..4d4ed3c35c 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -# The exhaustive tests are in test/controller/html/sanitizer_test.rb. +# The exhaustive tests are in the rails-html-sanitizer gem. # This tests that the helpers hook up correctly to the sanitizer classes. class SanitizeHelperTest < ActionView::TestCase tests ActionView::Helpers::SanitizeHelper @@ -10,6 +10,7 @@ class SanitizeHelperTest < ActionView::TestCase assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>") assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic") assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>") + assert_equal "<malformed & link", strip_links('<<a href="https://example.org">malformed & link</a>') end def test_sanitize_form @@ -26,6 +27,7 @@ class SanitizeHelperTest < ActionView::TestCase assert_equal("Dont touch me", strip_tags("Dont touch me")) assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>")) assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.") + assert_equal("Jekyll & Hyde", strip_tags("Jekyll & Hyde")) assert_equal "", strip_tags("<script>") end diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 3f51636603..3deddd5706 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -50,7 +50,7 @@ module ActionView end test "retrieve non existing config values" do - assert_equal nil, ActionView::Base.new.config.something_odd + assert_nil ActionView::Base.new.config.something_odd end test "works without testing a helper module" do @@ -285,7 +285,7 @@ module ActionView class AssertionsTest < ActionView::TestCase def render_from_helper form_tag("/foo") do - safe_concat render(text: "<ul><li>foo</li></ul>") + safe_concat render(plain: "<ul><li>foo</li></ul>") end end helper_method :render_from_helper diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index d77e4c6913..251c98230f 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -316,7 +316,7 @@ class TextHelperTest < ActionView::TestCase end def test_excerpt_with_omission - assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", omission: "[...]",radius: 5)) + assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", omission: "[...]", radius: 5)) assert_equal( "This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]", excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very", @@ -329,7 +329,7 @@ class TextHelperTest < ActionView::TestCase end def test_excerpt_does_not_modify_the_options_hash - options = { omission: "[...]",radius: 5 } + options = { omission: "[...]", radius: 5 } passed_options = options.dup excerpt("This is a beautiful morning", "beautiful", passed_options) assert_equal options, passed_options @@ -379,6 +379,8 @@ class TextHelperTest < ActionView::TestCase assert_equal("1.25 counts", pluralize("1.25", "count")) assert_equal("1.0 count", pluralize("1.0", "count")) assert_equal("1.00 count", pluralize("1.00", "count")) + assert_equal("2 counters", pluralize(2, "count", "counters")) + assert_equal("0 counters", pluralize(nil, "count", "counters")) assert_equal("2 counters", pluralize(2, "count", plural: "counters")) assert_equal("0 counters", pluralize(nil, "count", plural: "counters")) assert_equal("2 people", pluralize(2, "person")) @@ -405,12 +407,6 @@ class TextHelperTest < ActionView::TestCase end end - def test_deprecated_plural_as_positional_argument - assert_deprecated do - pluralize(2, "count", "counters") - end - end - def test_cycle_class value = Cycle.new("one", 2, "3") assert_equal("one", value.to_s) @@ -467,11 +463,11 @@ class TextHelperTest < ActionView::TestCase end def test_current_cycle_with_default_name - cycle("even","odd") + cycle("even", "odd") assert_equal "even", current_cycle - cycle("even","odd") + cycle("even", "odd") assert_equal "odd", current_cycle - cycle("even","odd") + cycle("even", "odd") assert_equal "even", current_cycle end @@ -490,13 +486,13 @@ class TextHelperTest < ActionView::TestCase end def test_current_cycle_with_more_than_two_names - cycle(1,2,3) + cycle(1, 2, 3) assert_equal "1", current_cycle - cycle(1,2,3) + cycle(1, 2, 3) assert_equal "2", current_cycle - cycle(1,2,3) + cycle(1, 2, 3) assert_equal "3", current_cycle - cycle(1,2,3) + cycle(1, 2, 3) assert_equal "1", current_cycle end diff --git a/actionview/test/template/text_test.rb b/actionview/test/template/text_test.rb index 6510688f97..72ffd5f3be 100644 --- a/actionview/test/template/text_test.rb +++ b/actionview/test/template/text_test.rb @@ -1,17 +1,23 @@ require "abstract_unit" class TextTest < ActiveSupport::TestCase - test "formats returns symbol for recognized MIME type" do - assert_equal [:text], ActionView::Template::Text.new("", :text).formats + test "formats always return :text" do + assert_equal [:text], ActionView::Template::Text.new("").formats end - test "formats returns string for recognized MIME type when MIME does not have symbol" do - foo = Mime::Type.lookup("foo") - assert_nil foo.to_sym - assert_equal ["foo"], ActionView::Template::Text.new("", foo).formats + test "identifier should return 'text template'" do + assert_equal "text template", ActionView::Template::Text.new("").identifier end - test "formats returns string for unknown MIME type" do - assert_equal ["foo"], ActionView::Template::Text.new("", "foo").formats + test "inspect should return 'text template'" do + assert_equal "text template", ActionView::Template::Text.new("").inspect + end + + test "to_str should return a given string" do + assert_equal "a cat", ActionView::Template::Text.new("a cat").to_str + end + + test "render should return a given string" do + assert_equal "a dog", ActionView::Template::Text.new("a dog").render end end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 2ef2be65d2..58d903b1c8 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -221,6 +221,37 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + class FakeParams + def initialize(permitted = true) + @permitted = permitted + end + + def permitted? + @permitted + end + + def to_h + if permitted? + { foo: :bar, baz: "quux" } + else + raise ArgumentError + end + end + end + + def test_button_to_with_permitted_strong_params + assert_dom_equal( + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>}, + button_to("Hello", "http://www.example.com", params: FakeParams.new) + ) + end + + def test_button_to_with_unpermitted_strong_params + assert_raises(ArgumentError) do + button_to("Hello", "http://www.example.com", params: FakeParams.new(false)) + end + end + def test_button_to_with_nested_hash_params assert_dom_equal( %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[bar]" value="baz" /></form>}, @@ -469,6 +500,21 @@ class UrlHelperTest < ActiveSupport::TestCase assert current_page?("http://www.example.com/") end + def test_current_page_considering_params + @request = request_for_url("/?order=desc&page=1") + + assert !current_page?(url_hash, check_parameters: true) + assert !current_page?(url_hash.merge(check_parameters: true)) + assert !current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!) + assert !current_page?("http://www.example.com/", check_parameters: true) + end + + def test_current_page_considering_params_when_options_does_not_respond_to_to_hash + @request = request_for_url("/?order=desc&page=1") + + assert !current_page?(:back, check_parameters: false) + end + def test_current_page_with_params_that_match @request = request_for_url("/?order=desc&page=1") @@ -569,8 +615,8 @@ class UrlHelperTest < ActiveSupport::TestCase def test_mail_to_with_special_characters assert_dom_equal( - %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C%7E@example.org">#!$%&'*+-/=?^_`{}|~@example.org</a>}, - mail_to("#!$%&'*+-/=?^_`{}|~@example.org") + %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C@example.org">#!$%&'*+-/=?^_`{}|@example.org</a>}, + mail_to("#!$%&'*+-/=?^_`{}|@example.org") ) end @@ -636,13 +682,6 @@ class UrlHelperTest < ActiveSupport::TestCase def request_forgery_protection_token "form_token" end - - private - def sort_query_string_params(uri) - path, qs = uri.split("?") - qs = qs.split("&").sort.join("&") if qs - qs ? "#{path}?#{qs}" : path - end end class UrlHelperControllerTest < ActionController::TestCase @@ -783,9 +822,9 @@ class TasksController < ActionController::Base render_default end - protected + private def render_default - render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" + + render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" \ "<%= link_to_unless_current('tasks', tasks_url) %>" end end @@ -840,6 +879,11 @@ class WorkshopsController < ActionController::Base @workshop = Workshop.new(params[:id]) render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>" end + + def edit + @workshop = Workshop.new(params[:id]) + render inline: "<%= current_page?(@workshop) %>" + end end class SessionsController < ActionController::Base @@ -904,4 +948,11 @@ class PolymorphicControllerTest < ActionController::TestCase get :edit, params: { workshop_id: 1, id: 1, format: "json" } assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body end + + def test_current_page_when_options_does_not_respond_to_to_hash + @controller = WorkshopsController.new + + get :edit, params: { id: 1 } + assert_equal "false", @response.body + end end diff --git a/actionview/test/ujs/.gitignore b/actionview/test/ujs/.gitignore new file mode 100644 index 0000000000..31dbbff57c --- /dev/null +++ b/actionview/test/ujs/.gitignore @@ -0,0 +1 @@ +/log diff --git a/actionview/test/ujs/config.ru b/actionview/test/ujs/config.ru new file mode 100644 index 0000000000..213a41127a --- /dev/null +++ b/actionview/test/ujs/config.ru @@ -0,0 +1,4 @@ +$LOAD_PATH.unshift __dir__ +require "server" + +run UJS::Server diff --git a/actionview/test/ujs/public/test/.eslintrc.yml b/actionview/test/ujs/public/test/.eslintrc.yml new file mode 100644 index 0000000000..06d7dd36ea --- /dev/null +++ b/actionview/test/ujs/public/test/.eslintrc.yml @@ -0,0 +1,21 @@ +env: + browser: true +extends: eslint:recommended +rules: + no-undef: off + no-unused-vars: off + indent: off + linebreak-style: ['error', 'unix'] + quotes: ['error', 'single'] + semi: ['error', 'never'] + no-shadow: ['error'] # Prevent potential errors + no-console: 'off' + # styles + space-before-function-paren: ['error', 'never'] + space-before-blocks: 'error' + brace-style: ['error', '1tbs', { allowSingleLine: true }] + key-spacing: 'error' + array-bracket-spacing: 'error' + comma-spacing: 'error' + comma-dangle: 'off' + eol-last: 'error' diff --git a/actionview/test/ujs/public/test/call-remote-callbacks.js b/actionview/test/ujs/public/test/call-remote-callbacks.js new file mode 100644 index 0000000000..707e21541d --- /dev/null +++ b/actionview/test/ujs/public/test/call-remote-callbacks.js @@ -0,0 +1,273 @@ +(function() { + +module('call-remote-callbacks', { + setup: function() { + $('#qunit-fixture').append($('<form />', { + action: '/echo', method: 'get', 'data-remote': 'true' + })) + }, + teardown: function() { + $(document).undelegate('form[data-remote]', 'ajax:beforeSend') + $(document).undelegate('form[data-remote]', 'ajax:before') + $(document).undelegate('form[data-remote]', 'ajax:send') + $(document).undelegate('form[data-remote]', 'ajax:complete') + $(document).undelegate('form[data-remote]', 'ajax:success') + $(document).unbind('ajaxStop') + $(document).unbind('iframe:loading') + } +}) + +function start_after_submit(form) { + form.bindNative('ajax:complete', function() { + ok(true, 'ajax:complete') + start() + }) +} + +function submit(fn) { + var form = $('form') + start_after_submit(form) + + if (fn) fn(form) + form.triggerNative('submit') +} + +function submit_with_button(submit_button) { + var form = $('form') + start_after_submit(form) + + submit_button.triggerNative('click') +} + +asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function() { + $('form[data-remote]') + .append($('<input type="text" name="user_name" value="john">')) + .append($('<input type="text" name="removed_user_name" value="john">')) + .bindNative('ajax:before', function() { + var form = $(this) + form + .append($('<input />', {name: 'other_user_name', value: 'jonathan'})) + .find('input[name="removed_user_name"]').remove() + form + .find('input[name="user_name"]').val('steve') + }) + + submit(function(form) { + form.bindNative('ajax:success', function(e, data, status, xhr) { + equal(data.params.user_name, 'steve', 'modified field value should have been submitted') + equal(data.params.other_user_name, 'jonathan', 'added field value should have been submitted') + equal(data.params.removed_user_name, undefined, 'removed field value should be undefined') + }) + }) +}) + +asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 2, function() { + $('form[data-remote]').data('type', 'html') + .bindNative('ajax:before', function() { + this.setAttribute('data-type', 'xml') + }) + + submit(function(form) { + form.bindNative('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.dataType, 'xml', 'modified dataType should have been requested') + }) + }) +}) + +asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function() { + $('form[data-remote]').data('with-credentials', false) + .bindNative('ajax:before', function() { + this.setAttribute('data-with-credentials', true) + }) + + submit(function(form) { + form.bindNative('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.withCredentials, true, 'setting modified in ajax:before should have forced withCredentials request') + }) + }) +}) + +asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() { + submit(function(form) { + form.bindNative('ajax:beforeSend', function() { + ok(true, 'aborting request in ajax:beforeSend') + return false + }) + form.unbind('ajax:send').bindNative('ajax:send', function() { + ok(false, 'ajax:send should not run') + }) + form.unbind('ajax:complete').bindNative('ajax:complete', function() { + ok(false, 'ajax:complete should not run') + }) + form.bindNative('ajax:error', function(e, xhr, status, error) { + ok(false, 'ajax:error should not run') + }) + $(document).bindNative('ajaxStop', function() { + start() + }) + }) +}) + +function skipIt() { + // This test cannot work due to the security feature in browsers which makes the value + // attribute of file input fields readonly, so it cannot be set with default value. + // This is what the test would look like though if browsers let us automate this test. + asyncTest('non-blank file form input field should abort remote request, but submit normally', 5, function() { + var form = $('form[data-remote]') + .append($('<input type="file" name="attachment" value="default.png">')) + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run') + }) + .bind('iframe:loading', function() { + ok(true, 'form should get submitted') + }) + .bindNative('ajax:aborted:file', function(e, data) { + ok(data.length == 1, 'ajax:aborted:file event is passed all non-blank file inputs (jQuery objects)') + ok(data.first().is('input[name="attachment"]'), 'ajax:aborted:file adds non-blank file input to data') + ok(true, 'ajax:aborted:file event should run') + }) + .triggerNative('submit') + + setTimeout(function() { + form.find('input[type="file"]').val('') + form.unbind('ajax:beforeSend') + submit() + }, 13) + }) + + asyncTest('file form input field should not abort remote request if file form input does not have a name attribute', 5, function() { + var form = $('form[data-remote]') + .append($('<input type="file" value="default.png">')) + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run') + }) + .bind('iframe:loading', function() { + ok(true, 'form should get submitted') + }) + .bindNative('ajax:aborted:file', function(e, data) { + ok(false, 'ajax:aborted:file should not run') + }) + .triggerNative('submit') + + setTimeout(function() { + form.find('input[type="file"]').val('') + form.unbind('ajax:beforeSend') + submit() + }, 13) + }) + + asyncTest('blank file input field should abort request entirely if handler bound to "ajax:aborted:file" event that returns false', 1, function() { + var form = $('form[data-remote]') + .append($('<input type="file" name="attachment" value="default.png">')) + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run') + }) + .bind('iframe:loading', function() { + ok(false, 'form should not get submitted') + }) + .bindNative('ajax:aborted:file', function() { + return false + }) + .triggerNative('submit') + + setTimeout(function() { + form.find('input[type="file"]').val('') + form.unbind('ajax:beforeSend') + submit() + }, 13) + }) +} + +asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() { + $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend observed with event delegation') + return false + }) + + submit(function(form) { + form.unbind('ajax:send').bindNative('ajax:send', function() { + ok(false, 'ajax:send should not run') + }) + form.unbind('ajax:complete').bindNative('ajax:complete', function() { + ok(false, 'ajax:complete should not run') + }) + $(document).bindNative('ajaxStop', function() { + start() + }) + }) +}) + +asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', 9, function() { + submit(function(form) { + form.bindNative('ajax:beforeSend', function(e, xhr, settings) { + ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object') + equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object') + }) + form.bindNative('ajax:send', function(e, xhr) { + ok(xhr.abort, 'first argument to "ajax:send" should be an XHR object') + }) + form.bindNative('ajax:success', function(e, data, status, xhr) { + ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object') + equal(status, 'OK', 'second argument to ajax:success should be a status string') + ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object') + }) + form.bindNative('ajax:complete', function(e, xhr, status) { + ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object') + equal(status, 'OK', 'second argument to ajax:complete should be a status string') + }) + }) +}) + +if(window.phantom !== undefined) { + asyncTest('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', 7, function() { + submit(function(form) { + form.attr('action', '/error') + form.bindNative('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }) + form.bindNative('ajax:send', function(arg) { ok(true, 'ajax:send') }) + form.bindNative('ajax:error', function(e, xhr, status, error) { + ok(xhr.getResponseHeader, 'first argument to "ajax:error" should be an XHR object') + equal(status, 'error', 'second argument to ajax:error should be a status string') + // Firefox 8 returns "Forbidden " with trailing space + equal($.trim(error), 'Forbidden', 'third argument to ajax:error should be an HTTP status response') + // Opera returns "0" for HTTP code + equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403') + }) + }) + }) +} + +// IF THIS TEST IS FAILING, TRY INCREASING THE TIMEOUT AT THE BOTTOM TO > 100 +asyncTest('binding to ajax callbacks via .delegate() triggers handlers properly', 4, function() { + $(document) + .delegate('form[data-remote]', 'ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend handler is triggered') + }) + .delegate('form[data-remote]', 'ajax:send', function() { + ok(true, 'ajax:send handler is triggered') + }) + .delegate('form[data-remote]', 'ajax:complete', function() { + ok(true, 'ajax:complete handler is triggered') + }) + .delegate('form[data-remote]', 'ajax:success', function() { + ok(true, 'ajax:success handler is triggered') + }) + $('form[data-remote]').triggerNative('submit') + + setTimeout(function() { + start() + }, 63) +}) + +asyncTest('binding to ajax:send event to call jquery methods on ajax object', 2, function() { + $('form[data-remote]') + .bindNative('ajax:send', function(e, xhr) { + ok(true, 'event should fire') + equal(typeof(xhr.abort), 'function', 'event should pass jqXHR object') + xhr.abort() + }) + .triggerNative('submit') + + setTimeout(function() { start() }, 35) +}) + +})() diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js new file mode 100644 index 0000000000..5932195363 --- /dev/null +++ b/actionview/test/ujs/public/test/call-remote.js @@ -0,0 +1,275 @@ +(function() { + +function buildForm(attrs) { + attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs) + + $('#qunit-fixture').append($('<form />', attrs)) + .find('form').append($('<input type="text" name="user_name" value="john">')) +} + +module('call-remote') + +function submit(fn) { + $('form') + .bindNative('ajax:success', fn) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('submit') +} + +asyncTest('form method is read from "method" and not from "data-method"', 1, function() { + buildForm({ method: 'post', 'data-method': 'get' }) + + submit(function(e, data, status, xhr) { + App.assertPostRequest(data) + }) +}) + +asyncTest('form method is not read from "data-method" attribute in case of missing "method"', 1, function() { + buildForm({ 'data-method': 'put' }) + + submit(function(e, data, status, xhr) { + App.assertGetRequest(data) + }) +}) + +asyncTest('form method is read from submit button "formmethod" if submit is triggered by that button', 1, function() { + var submitButton = $('<input type="submit" formmethod="get">') + buildForm({ method: 'post' }) + + $('#qunit-fixture').find('form').append(submitButton) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertGetRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + + submitButton.triggerNative('click') +}) + +asyncTest('form default method is GET', 1, function() { + buildForm() + + submit(function(e, data, status, xhr) { + App.assertGetRequest(data) + }) +}) + +asyncTest('form url is picked up from "action"', 1, function() { + buildForm({ method: 'post' }) + + submit(function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo') + }) +}) + +asyncTest('form url is read from "action" not "href"', 1, function() { + buildForm({ method: 'post', href: '/echo2' }) + + submit(function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo') + }) +}) + +asyncTest('form url is read from submit button "formaction" if submit is triggered by that button', 1, function() { + var submitButton = $('<input type="submit" formaction="/echo">') + buildForm({ method: 'post', href: '/echo2' }) + + $('#qunit-fixture').find('form').append(submitButton) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo') + }) + .bindNative('ajax:complete', function() { start() }) + + submitButton.triggerNative('click') +}) + +asyncTest('prefer JS, but accept any format', 1, function() { + buildForm({ method: 'post' }) + + submit(function(e, data, status, xhr) { + var accept = data.HTTP_ACCEPT + ok(accept.match(/text\/javascript.+\*\/\*/), 'Accept: ' + accept) + }) +}) + +asyncTest('JS code should be executed', 1, function() { + buildForm({ method: 'post', 'data-type': 'script' }) + + $('form').append('<input type="text" name="content_type" value="text/javascript">') + $('form').append('<input type="text" name="content" value="ok(true, \'remote code should be run\')">') + + submit() +}) + +asyncTest('ecmascript code should be executed', 1, function() { + buildForm({ method: 'post', 'data-type': 'script' }) + + $('form').append('<input type="text" name="content_type" value="application/ecmascript">') + $('form').append('<input type="text" name="content" value="ok(true, \'remote code should be run\')">') + + submit() +}) + +asyncTest('execution of JS code does not modify current DOM', 1, function() { + var docLength, newDocLength + function getDocLength() { + return document.documentElement.outerHTML.length + } + + buildForm({ method: 'post', 'data-type': 'script' }) + + $('form').append('<input type="text" name="content_type" value="text/javascript">') + $('form').append('<input type="text" name="content" value="\'remote code should be run\'">') + + docLength = getDocLength() + + submit(function() { + newDocLength = getDocLength() + ok(docLength === newDocLength, 'executed JS should not present in the document') + }) +}) + +asyncTest('XML document should be parsed', 1, function() { + buildForm({ method: 'post', 'data-type': 'html' }) + + $('form').append('<input type="text" name="content_type" value="application/xml">') + $('form').append('<input type="text" name="content" value="<p>hello</p>">') + + submit(function(e, data, status, xhr) { + ok(data instanceof Document, 'returned data should be an XML document') + }) +}) + +asyncTest('accept application/json if "data-type" is json', 1, function() { + buildForm({ method: 'post', 'data-type': 'json' }) + + submit(function(e, data, status, xhr) { + equal(data.HTTP_ACCEPT, 'application/json, text/javascript, */*; q=0.01') + }) +}) + +asyncTest('allow empty "data-remote" attribute', 1, function() { + var form = $('#qunit-fixture').append($('<form action="/echo" data-remote />')).find('form') + + submit(function() { + ok(true, 'form with empty "data-remote" attribute is also allowed') + }) +}) + +asyncTest('query string in form action should be stripped in a GET request in normal submit', 1, function() { + buildForm({ action: '/echo?param1=abc', 'data-remote': 'false' }) + + $(document).one('iframe:loaded', function(e, data) { + equal(data.params.param1, undefined, '"param1" should not be passed to server') + start() + }) + + $('#qunit-fixture form').triggerNative('submit') +}) + +asyncTest('query string in form action should be stripped in a GET request in ajax submit', 1, function() { + buildForm({ action: '/echo?param1=abc' }) + + submit(function(e, data, status, xhr) { + equal(data.params.param1, undefined, '"param1" should not be passed to server') + }) +}) + +asyncTest('query string in form action should not be stripped in a POST request in normal submit', 1, function() { + buildForm({ action: '/echo?param1=abc', method: 'post', 'data-remote': 'false' }) + + $(document).one('iframe:loaded', function(e, data) { + equal(data.params.param1, 'abc', '"param1" should be passed to server') + start() + }) + + $('#qunit-fixture form').triggerNative('submit') +}) + +asyncTest('query string in form action should not be stripped in a POST request in ajax submit', 1, function() { + buildForm({ action: '/echo?param1=abc', method: 'post' }) + + submit(function(e, data, status, xhr) { + equal(data.params.param1, 'abc', '"param1" should be passed to server') + }) +}) + +asyncTest('allow empty form "action"', 1, function() { + var currentLocation, ajaxLocation + + buildForm({ action: '' }) + + $('#qunit-fixture').find('form') + .bindNative('ajax:beforeSend', function(e, xhr, settings) { + // Get current location (the same way jQuery does) + try { + currentLocation = location.href + } catch(err) { + currentLocation = document.createElement( 'a' ) + currentLocation.href = '' + currentLocation = currentLocation.href + } + currentLocation = currentLocation.replace(/\?.*$/, '') + + // Actual location (strip out settings.data that jQuery serializes and appends) + // HACK: can no longer use settings.data below to see what was appended to URL, as of + // jQuery 1.6.3 (see http://bugs.jquery.com/ticket/10202 and https://github.com/jquery/jquery/pull/544) + ajaxLocation = settings.url.replace('user_name=john', '').replace(/&$/, '').replace(/\?$/, '') + equal(ajaxLocation.match(/^(.*)/)[1], currentLocation, 'URL should be current page by default') + + // Prevent the request from actually getting sent to the current page and + // causing an error. + return false + }) + .triggerNative('submit') + + setTimeout(function() { start() }, 13) +}) + +asyncTest('sends CSRF token in custom header', 1, function() { + buildForm({ method: 'post' }) + $('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />') + + submit(function(e, data, status, xhr) { + equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent') + }) +}) + +asyncTest('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', 1, function() { + + // Don't set data-cross-domain here, just set action to be a different domain than localhost + buildForm({ action: 'http://www.alfajango.com' }) + $('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />') + + $('#qunit-fixture').find('form') + .bindNative('ajax:beforeSend', function(evt, req, settings) { + + equal(settings.crossDomain, true, 'crossDomain should be set to true') + + // prevent request from actually getting sent off-domain + return false + }) + .triggerNative('submit') + + setTimeout(function() { start() }, 13) +}) + +asyncTest('intelligently guesses crossDomain behavior when target URL consists of only a path', 1, function() { + + // Don't set data-cross-domain here, just set action to be a different domain than localhost + buildForm({ action: '/just/a/path' }) + $('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />') + + $('#qunit-fixture').find('form') + .bindNative('ajax:beforeSend', function(evt, req, settings) { + + equal(settings.crossDomain, false, 'crossDomain should be set to false') + + // prevent request from actually getting sent off-domain + return false + }) + .triggerNative('submit') + + setTimeout(function() { start() }, 13) +}) + +})() diff --git a/actionview/test/ujs/public/test/csrf-refresh.js b/actionview/test/ujs/public/test/csrf-refresh.js new file mode 100644 index 0000000000..e302042542 --- /dev/null +++ b/actionview/test/ujs/public/test/csrf-refresh.js @@ -0,0 +1,24 @@ +(function() { + +module('csrf-refresh', {}) + +asyncTest('refresh all csrf tokens', 1, function() { + var correctToken = 'cf50faa3fe97702ca1ae' + + var form = $('<form />') + var input = $('<input>').attr({ type: 'hidden', name: 'authenticity_token', id: 'authenticity_token', value: 'foo' }) + input.appendTo(form) + + $('#qunit-fixture') + .append('<meta name="csrf-param" content="authenticity_token"/>') + .append('<meta name="csrf-token" content="' + correctToken + '"/>') + .append(form) + + $.rails.refreshCSRFTokens() + currentToken = $('#qunit-fixture #authenticity_token').val() + + start() + equal(currentToken, correctToken) +}) + +})() diff --git a/actionview/test/ujs/public/test/csrf-token.js b/actionview/test/ujs/public/test/csrf-token.js new file mode 100644 index 0000000000..388b40e057 --- /dev/null +++ b/actionview/test/ujs/public/test/csrf-token.js @@ -0,0 +1,27 @@ +(function() { + +module('csrf-token', {}) + +asyncTest('find csrf token', 1, function() { + var correctToken = 'cf50faa3fe97702ca1ae' + + $('#qunit-fixture').append('<meta name="csrf-token" content="' + correctToken + '"/>') + + currentToken = $.rails.csrfToken() + + start() + equal(currentToken, correctToken) +}) + +asyncTest('find csrf param', 1, function() { + var correctParam = 'authenticity_token' + + $('#qunit-fixture').append('<meta name="csrf-param" content="' + correctParam + '"/>') + + currentParam = $.rails.csrfParam() + + start() + equal(currentParam, correctParam) +}) + +})() diff --git a/actionview/test/ujs/public/test/data-confirm.js b/actionview/test/ujs/public/test/data-confirm.js new file mode 100644 index 0000000000..229b9e1466 --- /dev/null +++ b/actionview/test/ujs/public/test/data-confirm.js @@ -0,0 +1,316 @@ +module('data-confirm', { + setup: function() { + $('#qunit-fixture').append($('<a />', { + href: '/echo', + 'data-remote': 'true', + 'data-confirm': 'Are you absolutely sure?', + text: 'my social security number' + })) + + $('#qunit-fixture').append($('<button />', { + 'data-url': '/echo', + 'data-remote': 'true', + 'data-confirm': 'Are you absolutely sure?', + text: 'Click me' + })) + + $('#qunit-fixture').append($('<form />', { + id: 'confirm', + action: '/echo', + 'data-remote': 'true' + })) + + $('#qunit-fixture').append($('<input />', { + type: 'submit', + form: 'confirm', + 'data-confirm': 'Are you absolutely sure?' + })) + + $('#qunit-fixture').append($('<button />', { + type: 'submit', + form: 'confirm', + disabled: 'disabled', + 'data-confirm': 'Are you absolutely sure?' + })) + + this.windowConfirm = window.confirm + }, + teardown: function() { + window.confirm = this.windowConfirm + } +}) + +asyncTest('clicking on a link with data-confirm attribute. Confirm yes.', 6, function() { + var message + // auto-confirm: + window.confirm = function(msg) { message = msg; return true } + + $('a[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == true, 'confirm:complete passes in confirm answer (true)') + }) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + App.assertGetRequest(data) + + equal(message, 'Are you absolutely sure?') + start() + }) + .triggerNative('click') +}) + +asyncTest('clicking on a button with data-confirm attribute. Confirm yes.', 6, function() { + var message + // auto-confirm: + window.confirm = function(msg) { message = msg; return true } + + $('button[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == true, 'confirm:complete passes in confirm answer (true)') + }) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + App.assertGetRequest(data) + + equal(message, 'Are you absolutely sure?') + start() + }) + .triggerNative('click') +}) + +asyncTest('clicking on a link with data-confirm attribute. Confirm No.', 3, function() { + var message + // auto-decline: + window.confirm = function(msg) { message = msg; return false } + + $('a[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == false, 'confirm:complete passes in confirm answer (false)') + }) + .bindNative('ajax:beforeSend', function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:beforeSend') + }) + .triggerNative('click') + + setTimeout(function() { + equal(message, 'Are you absolutely sure?') + start() + }, 50) +}) + +asyncTest('clicking on a button with data-confirm attribute. Confirm No.', 3, function() { + var message + // auto-decline: + window.confirm = function(msg) { message = msg; return false } + + $('button[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == false, 'confirm:complete passes in confirm answer (false)') + }) + .bindNative('ajax:beforeSend', function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:beforeSend') + }) + .triggerNative('click') + + setTimeout(function() { + equal(message, 'Are you absolutely sure?') + start() + }, 50) +}) + +asyncTest('clicking on a button with data-confirm attribute. Confirm error.', 3, function() { + var message + // auto-decline: + window.confirm = function(msg) { message = msg; throw 'some random error' } + + $('button[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == false, 'confirm:complete passes in confirm answer (false)') + }) + .bindNative('ajax:beforeSend', function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:beforeSend') + }) + .triggerNative('click') + + setTimeout(function() { + equal(message, 'Are you absolutely sure?') + start() + }, 50) +}) + +asyncTest('clicking on a submit button with form and data-confirm attributes. Confirm No.', 3, function() { + var message + // auto-decline: + window.confirm = function(msg) { message = msg; return false } + + $('input[type=submit][form]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == false, 'confirm:complete passes in confirm answer (false)') + }) + .bindNative('ajax:beforeSend', function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:beforeSend') + }) + .triggerNative('click') + + setTimeout(function() { + equal(message, 'Are you absolutely sure?') + start() + }, 50) +}) + +asyncTest('binding to confirm event of a link and returning false', 1, function() { + // redefine confirm function so we can make sure it's not called + window.confirm = function(msg) { + ok(false, 'confirm dialog should not be called') + } + + $('a[data-confirm]') + .bindNative('confirm', function() { + App.assertCallbackInvoked('confirm') + return false + }) + .bindNative('confirm:complete', function() { + App.assertCallbackNotInvoked('confirm:complete') + }) + .triggerNative('click') + + setTimeout(function() { + start() + }, 50) +}) + +asyncTest('binding to confirm event of a button and returning false', 1, function() { + // redefine confirm function so we can make sure it's not called + window.confirm = function(msg) { + ok(false, 'confirm dialog should not be called') + } + + $('button[data-confirm]') + .bindNative('confirm', function() { + App.assertCallbackInvoked('confirm') + return false + }) + .bindNative('confirm:complete', function() { + App.assertCallbackNotInvoked('confirm:complete') + }) + .triggerNative('click') + + setTimeout(function() { + start() + }, 50) +}) + +asyncTest('binding to confirm:complete event of a link and returning false', 2, function() { + // auto-confirm: + window.confirm = function(msg) { + ok(true, 'confirm dialog should be called') + return true + } + + $('a[data-confirm]') + .bindNative('confirm:complete', function() { + App.assertCallbackInvoked('confirm:complete') + return false + }) + .bindNative('ajax:beforeSend', function() { + App.assertCallbackNotInvoked('ajax:beforeSend') + }) + .triggerNative('click') + + setTimeout(function() { + start() + }, 50) +}) + +asyncTest('binding to confirm:complete event of a button and returning false', 2, function() { + // auto-confirm: + window.confirm = function(msg) { + ok(true, 'confirm dialog should be called') + return true + } + + $('button[data-confirm]') + .bindNative('confirm:complete', function() { + App.assertCallbackInvoked('confirm:complete') + return false + }) + .bindNative('ajax:beforeSend', function() { + App.assertCallbackNotInvoked('ajax:beforeSend') + }) + .triggerNative('click') + + setTimeout(function() { + start() + }, 50) +}) + +asyncTest('a button inside a form only confirms once', 1, function() { + var confirmations = 0 + window.confirm = function(msg) { + confirmations++ + return true + } + + $('#qunit-fixture').append($('<form />').append($('<button />', { + 'data-remote': 'true', + 'data-confirm': 'Are you absolutely sure?', + text: 'Click me' + }))) + + $('form > button[data-confirm]').triggerNative('click') + + ok(confirmations === 1, 'confirmation counter should be 1, but it was ' + confirmations) + start() +}) + +asyncTest('clicking on the children of a link should also trigger a confirm', 6, function() { + var message + // auto-confirm: + window.confirm = function(msg) { message = msg; return true } + + $('a[data-confirm]') + .html('<strong>Click me</strong>') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == true, 'confirm:complete passes in confirm answer (true)') + }) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + App.assertGetRequest(data) + + equal(message, 'Are you absolutely sure?') + start() + }) + .find('strong') + .triggerNative('click') +}) + +asyncTest('clicking on the children of a disabled button should not trigger a confirm.', 1, function() { + var message + // auto-decline: + window.confirm = function(msg) { message = msg; return false } + + $('button[data-confirm][disabled]') + .html("<strong>Click me</strong>") + .bindNative('confirm', function() { + App.assertCallbackNotInvoked('confirm') + }) + .find('strong') + .bindNative('click', function() { + App.assertCallbackInvoked('click') + }) + .triggerNative('click') + + setTimeout(function() { + start() + }, 50) +}) diff --git a/actionview/test/ujs/public/test/data-disable-with.js b/actionview/test/ujs/public/test/data-disable-with.js new file mode 100644 index 0000000000..b29cbbc867 --- /dev/null +++ b/actionview/test/ujs/public/test/data-disable-with.js @@ -0,0 +1,391 @@ +module('data-disable-with', { + setup: function() { + $('#qunit-fixture').append($('<form />', { + action: '/echo', + 'data-remote': 'true', + method: 'post' + })) + .find('form') + .append($('<input type="text" data-disable-with="processing ..." name="user_name" value="john" />')) + + $('#qunit-fixture').append($('<form />', { + action: '/echo', + method: 'post', + id: 'not_remote' + })) + .find('form:last') + // WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!) + .append($('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />')) + + $('#qunit-fixture').append($('<a />', { + text: 'Click me', + href: '/echo', + 'data-disable-with': 'clicking...' + })) + + $('#qunit-fixture').append($('<input />', { + type: 'submit', + form: 'not_remote', + 'data-disable-with': 'form attr submitting', + name: 'submit3', + value: 'Form Attr Submit' + })) + + $('#qunit-fixture').append($('<button />', { + text: 'Click me', + 'data-remote': true, + 'data-url': '/echo', + 'data-disable-with': 'clicking...' + })) + }, + teardown: function() { + $(document).unbind('iframe:loaded') + } +}) + +asyncTest('form input field with "data-disable-with" attribute', 7, function() { + var form = $('form[data-remote]'), input = form.find('input[type=text]') + + App.checkEnabledState(input, 'john') + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(input, 'john') + equal(data.params.user_name, 'john') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(input, 'processing ...') +}) + +asyncTest('blank form input field with "data-disable-with" attribute', 7, function() { + var form = $('form[data-remote]'), input = form.find('input[type=text]') + + input.val('') + App.checkEnabledState(input, '') + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(input, '') + equal(data.params.user_name, '') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(input, 'processing ...') +}) + +asyncTest('form button with "data-disable-with" attribute', 6, function() { + var form = $('form[data-remote]'), button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>') + form.append(button) + + App.checkEnabledState(button, 'Submit') + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(button, 'Submit') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(button, 'submitting ...') +}) + +asyncTest('form input[type=submit][data-disable-with] disables', 6, function() { + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]') + + App.checkEnabledState(input, 'Submit') + + $(document).bind('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'submitting ...') + start() + }, 30) + }) + form.triggerNative('submit') + + setTimeout(function() { + App.checkDisabledState(input, 'submitting ...') + }, 30) +}) + +test('form input[type=submit][data-disable-with] re-enables when `pageshow` event is triggered', function() { + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]') + + App.checkEnabledState(input, 'Submit') + + // Emulate the disabled state without submitting the form at all, what is the + // state after going back on firefox after submitting a form. + // + // See https://github.com/rails/jquery-ujs/issues/357 + $.rails.disableElement(form[0]) + + App.checkDisabledState(input, 'submitting ...') + + $(window).triggerNative('pageshow') + + App.checkEnabledState(input, 'Submit') +}) + +asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', 2, function() { + var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html() + + form.bindNative('ajax:success', function() { + form.html(origFormContents) + + setTimeout(function() { + var input = form.find('input[type=submit]') + App.checkEnabledState(input, 'Submit') + start() + }, 30) + }).triggerNative('submit') +}) + +asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', 2, function() { + var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + newDisabledInput = input.clone().attr('disabled', 'disabled') + + form.bindNative('ajax:success', function() { + input.replaceWith(newDisabledInput) + + setTimeout(function() { + App.checkEnabledState(newDisabledInput, 'Submit') + start() + }, 30) + }).triggerNative('submit') +}) + +asyncTest('form input[type=submit][data-disable-with] using "form" attribute disables', 6, function() { + var form = $('#not_remote'), input = $('input[form=not_remote]') + App.checkEnabledState(input, 'Form Attr Submit') + + $(document).bind('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'form attr submitting') + start() + }, 30) + }) + form.triggerNative('submit') + + setTimeout(function() { + App.checkDisabledState(input, 'form attr submitting') + }, 30) + +}) + +asyncTest('form[data-remote] textarea[data-disable-with] attribute', 3, function() { + var form = $('form[data-remote]'), + textarea = $('<textarea data-disable-with="processing ..." name="user_bio">born, lived, died.</textarea>').appendTo(form) + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + equal(data.params.user_bio, 'born, lived, died.') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(textarea, 'processing ...') +}) + +asyncTest('a[data-disable-with] disables', 4, function() { + var link = $('a[data-disable-with]') + + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click') + App.checkDisabledState(link, 'clicking...') + start() +}) + +test('a[data-disable-with] re-enables when `pageshow` event is triggered', function() { + var link = $('a[data-disable-with]') + + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click') + App.checkDisabledState(link, 'clicking...') + + $(window).triggerNative('pageshow') + App.checkEnabledState(link, 'Click me') +}) + +asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true) + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...') + }) + .bindNative('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(link, 'Click me') + start() + }, 15) + }) + .triggerNative('click') +}) + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true) + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:before', function() { + App.checkDisabledState(link, 'clicking...') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true) + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error') + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...') + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) + +asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { + var form = $('form[data-remote]'), + input = form.find('input:text'), + button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>').appendTo(form), + textarea = $('<textarea data-disable-with="processing ..." name="user_bio">born, lived, died.</textarea>').appendTo(form), + submit = $('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />').appendTo(form) + + form + .bindNative('ajax:beforeSend', function() { + return false + }) + .triggerNative('submit') + + App.checkEnabledState(input, 'john') + App.checkEnabledState(button, 'Submit') + App.checkEnabledState(textarea, 'born, lived, died.') + App.checkEnabledState(submit, 'Submit') + + start() +}) + +asyncTest('ctrl-clicking on a link does not disables the link', 6, function() { + var link = $('a[data-disable-with]') + + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click', { metaKey: true }) + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click', { metaKey: true }) + App.checkEnabledState(link, 'Click me') + start() +}) + +asyncTest('button[data-remote][data-disable-with] disables and re-enables', 6, function() { + var button = $('button[data-remote][data-disable-with]') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'clicking...') + }) + .bindNative('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(button, 'Click me') + start() + }, 15) + }) + .triggerNative('click') +}) + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable-with]') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:before', function() { + App.checkDisabledState(button, 'clicking...') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable-with]') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(button, 'clicking...') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() { + var button = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'clicking...') + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) diff --git a/actionview/test/ujs/public/test/data-disable.js b/actionview/test/ujs/public/test/data-disable.js new file mode 100644 index 0000000000..ccc38cf9ae --- /dev/null +++ b/actionview/test/ujs/public/test/data-disable.js @@ -0,0 +1,321 @@ +module('data-disable', { + setup: function() { + $('#qunit-fixture').append($('<form />', { + action: '/echo', + 'data-remote': 'true', + method: 'post' + })) + .find('form') + .append($('<input type="text" data-disable name="user_name" value="john" />')) + + $('#qunit-fixture').append($('<form />', { + action: '/echo', + method: 'post' + })) + .find('form:last') + // WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!) + .append($('<input type="submit" data-disable name="submit2" value="Submit" />')) + + $('#qunit-fixture').append($('<a />', { + text: 'Click me', + href: '/echo', + 'data-disable': 'true' + })) + + $('#qunit-fixture').append($('<button />', { + text: 'Click me', + 'data-remote': true, + 'data-url': '/echo', + 'data-disable': 'true' + })) + }, + teardown: function() { + $(document).unbind('iframe:loaded') + } +}) + +asyncTest('form input field with "data-disable" attribute', 7, function() { + var form = $('form[data-remote]'), input = form.find('input[type=text]') + + App.checkEnabledState(input, 'john') + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(input, 'john') + equal(data.params.user_name, 'john') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(input, 'john') +}) + +asyncTest('form button with "data-disable" attribute', 7, function() { + var form = $('form[data-remote]'), button = $('<button data-disable name="submit2">Submit</button>') + form.append(button) + + App.checkEnabledState(button, 'Submit') + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(button, 'Submit') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(button, 'Submit') + equal(button.data('ujs:enable-with'), undefined) +}) + +asyncTest('form input[type=submit][data-disable] disables', 6, function() { + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]') + + App.checkEnabledState(input, 'Submit') + + // WEEIRDD: attaching this handler makes the test work in IE7 + $(document).bind('iframe:loading', function(e, f) {}) + + $(document).bind('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'Submit') + start() + }, 30) + }) + form.triggerNative('submit') + + setTimeout(function() { + App.checkDisabledState(input, 'Submit') + }, 30) +}) + +asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in ajax callback', 2, function() { + var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html() + + form.bindNative('ajax:success', function() { + form.html(origFormContents) + + setTimeout(function() { + var input = form.find('input[type=submit]') + App.checkEnabledState(input, 'Submit') + start() + }, 30) + }).triggerNative('submit') +}) + +asyncTest('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', 2, function() { + var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + newDisabledInput = input.clone().attr('disabled', 'disabled') + + form.bindNative('ajax:success', function() { + input.replaceWith(newDisabledInput) + + setTimeout(function() { + App.checkEnabledState(newDisabledInput, 'Submit') + start() + }, 30) + }).triggerNative('submit') +}) + +asyncTest('form[data-remote] textarea[data-disable] attribute', 3, function() { + var form = $('form[data-remote]'), + textarea = $('<textarea data-disable name="user_bio">born, lived, died.</textarea>').appendTo(form) + + form.bindNative('ajax:success', function(e, data) { + setTimeout(function() { + equal(data.params.user_bio, 'born, lived, died.') + start() + }, 13) + }) + form.triggerNative('submit') + + App.checkDisabledState(textarea, 'born, lived, died.') +}) + +asyncTest('a[data-disable] disables', 5, function() { + var link = $('a[data-disable]') + + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click') + App.checkDisabledState(link, 'Click me') + equal(link.data('ujs:enable-with'), undefined) + start() +}) + +asyncTest('a[data-remote][data-disable] disables and re-enables', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true) + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:send', function() { + App.checkDisabledState(link, 'Click me') + }) + .bindNative('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(link, 'Click me') + start() + }, 15) + }) + .triggerNative('click') +}) + +asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true) + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:before', function() { + App.checkDisabledState(link, 'Click me') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) + +asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true) + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'Click me') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) + +asyncTest('a[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/error') + + App.checkEnabledState(link, 'Click me') + + link + .bindNative('ajax:send', function() { + App.checkDisabledState(link, 'Click me') + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) + +asyncTest('form[data-remote] input|button|textarea[data-disable] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { + var form = $('form[data-remote]'), + input = form.find('input:text'), + button = $('<button data-disable="submitting ..." name="submit2">Submit</button>').appendTo(form), + textarea = $('<textarea data-disable name="user_bio">born, lived, died.</textarea>').appendTo(form), + submit = $('<input type="submit" data-disable="submitting ..." name="submit2" value="Submit" />').appendTo(form) + + form + .bindNative('ajax:beforeSend', function() { + return false + }) + .triggerNative('submit') + + App.checkEnabledState(input, 'john') + App.checkEnabledState(button, 'Submit') + App.checkEnabledState(textarea, 'born, lived, died.') + App.checkEnabledState(submit, 'Submit') + + start() +}) + +asyncTest('ctrl-clicking on a link does not disables the link', 6, function() { + var link = $('a[data-disable]') + + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click', { metaKey: true }) + App.checkEnabledState(link, 'Click me') + + link.triggerNative('click', { ctrlKey: true }) + App.checkEnabledState(link, 'Click me') + start() +}) + +asyncTest('button[data-remote][data-disable] disables and re-enables', 6, function() { + var button = $('button[data-remote][data-disable]') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'Click me') + }) + .bindNative('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(button, 'Click me') + start() + }, 15) + }) + .triggerNative('click') +}) + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable]') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:before', function() { + App.checkDisabledState(button, 'Click me') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable]') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(button, 'Click me') + return false + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() { + var button = $('a[data-disable]').attr('data-remote', true).attr('href', '/error') + + App.checkEnabledState(button, 'Click me') + + button + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'Click me') + }) + .triggerNative('click') + + setTimeout(function() { + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) diff --git a/actionview/test/ujs/public/test/data-method.js b/actionview/test/ujs/public/test/data-method.js new file mode 100644 index 0000000000..47d940c577 --- /dev/null +++ b/actionview/test/ujs/public/test/data-method.js @@ -0,0 +1,85 @@ +(function() { + +module('data-method', { + setup: function() { + $('#qunit-fixture').append($('<a />', { + href: '/echo', 'data-method': 'delete', text: 'destroy!' + })) + }, + teardown: function() { + $(document).unbind('iframe:loaded') + } +}) + +function submit(fn, options) { + $(document).bind('iframe:loaded', function(e, data) { + fn(data) + start() + }) + + $('#qunit-fixture').find('a') + .triggerNative('click') +} + +asyncTest('link with "data-method" set to "delete"', 3, function() { + submit(function(data) { + equal(data.REQUEST_METHOD, 'DELETE') + strictEqual(data.params.authenticity_token, undefined) + strictEqual(data.HTTP_X_CSRF_TOKEN, undefined) + }) +}) + +asyncTest('click on the child of link with "data-method"', 3, function() { + $(document).bind('iframe:loaded', function(e, data) { + equal(data.REQUEST_METHOD, 'DELETE') + strictEqual(data.params.authenticity_token, undefined) + strictEqual(data.HTTP_X_CSRF_TOKEN, undefined) + start() + }) + $('#qunit-fixture a').html('<strong>destroy!</strong>').find('strong').triggerNative('click') +}) + +asyncTest('link with "data-method" and CSRF', 1, function() { + $('#qunit-fixture') + .append('<meta name="csrf-param" content="authenticity_token"/>') + .append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>') + + submit(function(data) { + equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae') + }) +}) + +asyncTest('link "target" should be carried over to generated form', 1, function() { + $('a[data-method]').attr('target', 'super-special-frame') + submit(function(data) { + equal(data.params._target, 'super-special-frame') + }) +}) + +asyncTest('link with "data-method" and cross origin', 1, function() { + var data = {} + + $('#qunit-fixture') + .append('<meta name="csrf-param" content="authenticity_token"/>') + .append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>') + + $(document).on('submit', 'form', function(e) { + $(e.currentTarget).serializeArray().map(function(item) { + data[item.name] = item.value + }) + + return false + }) + + var link = $('#qunit-fixture').find('a') + + link.attr('href', 'http://www.alfajango.com') + + link.triggerNative('click') + + start() + + notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae') +}) + +})() diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js new file mode 100644 index 0000000000..161a92ac11 --- /dev/null +++ b/actionview/test/ujs/public/test/data-remote.js @@ -0,0 +1,438 @@ +(function() { + +function buildSelect(attrs) { + attrs = $.extend({ + 'name': 'user_data', 'data-remote': 'true', 'data-url': '/echo', 'data-params': 'data1=value1' + }, attrs) + + $('#qunit-fixture').append( + $('<select />', attrs) + .append($('<option />', {value: 'optionValue1', text: 'option1'})) + .append($('<option />', {value: 'optionValue2', text: 'option2'})) + ) +} + +module('data-remote', { + setup: function() { + $('#qunit-fixture') + .append($('<a />', { + href: '/echo', + 'data-remote': 'true', + 'data-params': 'data1=value1&data2=value2', + text: 'my address' + })) + .append($('<button />', { + 'data-url': '/echo', + 'data-remote': 'true', + 'data-params': 'data1=value1&data2=value2', + text: 'my button' + })) + .append($('<form />', { + action: '/echo', + 'data-remote': 'true', + method: 'post', + id: 'my-remote-form' + })) + .append($('<a />', { + href: '/echo', + 'data-remote': 'true', + disabled: 'disabled', + text: 'Disabed link' + })) + .find('form').append($('<input type="text" name="user_name" value="john">')) + + } +}) + +asyncTest('ctrl-clicking on a link does not fire ajaxyness', 0, function() { + var link = $('a[data-remote]') + + // Ideally, we'd setup an iframe to intercept normal link clicks + // and add a test to make sure the iframe:loaded event is triggered. + // However, jquery doesn't actually cause a native `click` event and + // follow links using `trigger('click')`, it only fires bindings. + link + .removeAttr('data-params') + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) + + link.triggerNative('click', { metaKey: true }) + link.triggerNative('click', { ctrlKey: true }) + + setTimeout(function() { start() }, 13) +}) + +asyncTest('ctrl-clicking on a link still fires ajax for non-GET links and for links with "data-params"', 2, function() { + var link = $('a[data-remote]') + + link + .removeAttr('data-params') + .attr('data-method', 'POST') + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax should be triggered') + }) + .triggerNative('click', { metaKey: true }) + + link + .removeAttr('data-method') + .attr('data-params', 'name=steve') + .triggerNative('click', { metaKey: true }) + + setTimeout(function() { start() }, 13) +}) + +asyncTest('clicking on a link with data-remote attribute', 5, function() { + $('a[data-remote]') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value') + equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value') + App.assertGetRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) + +asyncTest('clicking on a link with both query string in href and data-params', 4, function() { + $('a[data-remote]') + .attr('href', '/echo?data3=value3') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertGetRequest(data) + equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value') + equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value') + equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value') + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) + +asyncTest('clicking on a link with both query string in href and data-params with POST method', 4, function() { + $('a[data-remote]') + .attr('href', '/echo?data3=value3') + .attr('data-method', 'post') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertPostRequest(data) + equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value') + equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value') + equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value') + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) + +asyncTest('clicking on a link with disabled attribute', 0, function() { + $('a[disabled]') + .bindNative('ajax:before', function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:success') + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') + + setTimeout(function() { + start() + }, 13) +}) + +asyncTest('clicking on a button with data-remote attribute', 5, function() { + $('button[data-remote]') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value') + equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value') + App.assertGetRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) + +asyncTest('changing a select option with data-remote attribute', 5, function() { + buildSelect() + + $('select[data-remote]') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + equal(data.params.user_data, 'optionValue2', 'ajax arguments should have key term with right value') + equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value') + App.assertGetRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + .val('optionValue2') + .triggerNative('change') +}) + +asyncTest('submitting form with data-remote attribute', 4, function() { + $('form[data-remote]') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value') + App.assertPostRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('submit') +}) + +asyncTest('submitting form with data-remote attribute should include inputs in a fieldset only once', 3, function() { + $('form[data-remote]') + .append('<fieldset><input name="items[]" value="Item" /></fieldset>') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + equal(data.params.items.length, 1, 'ajax arguments should only have the item once') + App.assertPostRequest(data) + }) + .bindNative('ajax:complete', function() { + $('form[data-remote], fieldset').remove() + start() + }) + .triggerNative('submit') +}) + +asyncTest('submitting form with data-remote attribute submits input with matching [form] attribute', 5, function() { + $('#qunit-fixture') + .append($('<input type="text" name="user_data" value="value1" form="my-remote-form">')) + + $('form[data-remote]') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value') + equal(data.params.user_data, 'value1', 'ajax arguments should have key user_data with right value') + App.assertPostRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('submit') +}) + +asyncTest('submitting form with data-remote attribute by clicking button with matching [form] attribute', 5, function() { + $('form[data-remote]') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value') + equal(data.params.user_data, 'value2', 'ajax arguments should have key user_data with right value') + App.assertPostRequest(data) + }) + .bindNative('ajax:complete', function() { start() }) + + $('<button />', { + type: 'submit', + name: 'user_data', + value: 'value1', + form: 'my-remote-form' + }) + .appendTo($('#qunit-fixture')) + + $('<button />', { + type: 'submit', + name: 'user_data', + value: 'value2', + form: 'my-remote-form' + }) + .appendTo($('#qunit-fixture')) + .triggerNative('click') +}) + +asyncTest('form\'s submit bindings in browsers that don\'t support submit bubbling', 5, function() { + var form = $('form[data-remote]'), directBindingCalled = false + + ok(!directBindingCalled, 'nothing is called') + + form + .append($('<input type="submit" />')) + .bindNative('submit', function(event) { + ok(event.type == 'submit', 'submit event handlers are called with submit event') + ok(true, 'binding handler is called') + directBindingCalled = true + }) + .bindNative('ajax:beforeSend', function() { + ok(true, 'form being submitted via ajax') + ok(directBindingCalled, 'binding handler already called') + }) + .bindNative('ajax:complete', function() { + start() + }) + + if(!$.support.submitBubbles) { + // Must indrectly submit form via click to trigger jQuery's manual submit bubbling in IE + form.find('input[type=submit]') + .triggerNative('click') + } else { + form.triggerNative('submit') + } +}) + +asyncTest('returning false in form\'s submit bindings in non-submit-bubbling browsers', 1, function() { + var form = $('form[data-remote]') + + form + .append($('<input type="submit" />')) + .bindNative('submit', function() { + ok(true, 'binding handler is called') + return false + }) + .bindNative('ajax:beforeSend', function() { + ok(false, 'form should not be submitted') + }) + + if (!$.support.submitBubbles) { + // Must indrectly submit form via click to trigger jQuery's manual submit bubbling in IE + form.find('input[type=submit]').triggerNative('click') + } else { + form.triggerNative('submit') + } + + setTimeout(function() { start() }, 13) +}) + +asyncTest('clicking on a link with falsy "data-remote" attribute does not fire ajaxyness', 0, function() { + $('a[data-remote]') + .attr('data-remote', 'false') + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) + .bindNative('click', function() { + return false + }) + .triggerNative('click') + + setTimeout(function() { start() }, 20) +}) + +asyncTest('ctrl-clicking on a link with falsy "data-remote" attribute does not fire ajaxyness even if "data-params" present', 0, function() { + var link = $('a[data-remote]') + + link + .removeAttr('data-params') + .attr('data-remote', 'false') + .attr('data-method', 'POST') + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) + .bindNative('click', function() { + return false + }) + .triggerNative('click', { metaKey: true }) + + link + .removeAttr('data-method') + .attr('data-params', 'name=steve') + .triggerNative('click', { metaKey: true }) + + setTimeout(function() { start() }, 20) +}) + +asyncTest('clicking on a button with falsy "data-remote" attribute', 0, function() { + $('button[data-remote]:first') + .attr('data-remote', 'false') + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) + .bindNative('click', function() { + return false + }) + .triggerNative('click') + + setTimeout(function() { start() }, 20) +}) + +asyncTest('submitting a form with falsy "data-remote" attribute', 0, function() { + $('form[data-remote]:first') + .attr('data-remote', 'false') + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) + .bindNative('submit', function() { + return false + }) + .triggerNative('submit') + + setTimeout(function() { start() }, 20) +}) + +asyncTest('changing a select option with falsy "data-remote" attribute', 0, function() { + buildSelect({'data-remote': 'false'}) + + $('select[data-remote=false]:first') + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) + .val('optionValue2') + .triggerNative('change') + + setTimeout(function() { start() }, 20) +}) + +asyncTest('form should be serialized correctly', 6, function() { + $('form') + .append('<textarea name="textarea">textarea</textarea>') + .append('<input type="checkbox" name="checkbox[]" value="0" />') + .append('<input type="checkbox" checked="checked" name="checkbox[]" value="1" />') + .append('<input type="radio" checked="checked" name="radio" value="0" />') + .append('<input type="radio" name="radio" value="1" />') + .append('<select multiple="multiple" name="select[]">\ + <option value="1" selected>1</option>\ + <option value="2" selected>2</option>\ + <option value="3">3</option>\ + <option selected>4</option>\ + </select>') + .bindNative('ajax:success', function(e, data, status, xhr) { + equal(data.params.checkbox.length, 1) + equal(data.params.checkbox[0], '1') + equal(data.params.radio, '0') + equal(data.params.select.length, 3) + equal(data.params.select[2], '4') + equal(data.params.textarea, 'textarea') + + start() + }) + .triggerNative('submit') +}) + +asyncTest('form buttons should only be serialized when clicked', 4, function() { + $('form') + .append('<input type="submit" name="submit1" value="submit1" />') + .append('<button name="submit2" value="submit2" />') + .append('<button name="submit3" value="submit3" />') + .bindNative('ajax:success', function(e, data, status, xhr) { + equal(data.params.submit1, undefined) + equal(data.params.submit2, 'submit2') + equal(data.params.submit3, undefined) + equal(data['rack.request.form_vars'], 'user_name=john&submit2=submit2') + + start() + }) + .find('[name=submit2]').triggerNative('click') +}) + +asyncTest('changing a select option without "data-url" attribute still fires ajax request to current location', 1, function() { + var currentLocation, ajaxLocation + + buildSelect({'data-url': ''}); + + $('select[data-remote]') + .bindNative('ajax:beforeSend', function(e, xhr, settings) { + // Get current location (the same way jQuery does) + try { + currentLocation = location.href + } catch(err) { + currentLocation = document.createElement( 'a' ) + currentLocation.href = '' + currentLocation = currentLocation.href + } + + ajaxLocation = settings.url.replace(settings.data, '').replace(/&$/, '').replace(/\?$/, '') + equal(ajaxLocation, currentLocation, 'URL should be current page by default') + + return false + }) + .val('optionValue2') + .triggerNative('change') + + setTimeout(function() { start() }, 20) +}) + +})() diff --git a/actionview/test/ujs/public/test/override.js b/actionview/test/ujs/public/test/override.js new file mode 100644 index 0000000000..299c7018cc --- /dev/null +++ b/actionview/test/ujs/public/test/override.js @@ -0,0 +1,56 @@ +(function() { + +var realHref + +module('override', { + setup: function() { + realHref = $.rails.href + $('#qunit-fixture') + .append($('<a />', { + href: '/real/href', 'data-remote': 'true', 'data-method': 'delete', 'data-href': '/data/href' + })) + }, + teardown: function() { + $.rails.href = realHref + } +}) + +asyncTest('the getter for an element\'s href is publicly accessible', 1, function() { + ok($.rails.href) + start() +}) + +asyncTest('the getter for an element\'s href is overridable', 1, function() { + $.rails.href = function(element) { return $(element).data('href') } + $('#qunit-fixture a') + .bindNative('ajax:beforeSend', function(e, xhr, options) { + equal('/data/href', options.url) + return false + }) + .triggerNative('click') + start() +}) + +asyncTest('the getter for an element\'s href works normally if not overridden', 1, function() { + $('#qunit-fixture a') + .bindNative('ajax:beforeSend', function(e, xhr, options) { + equal(location.protocol + '//' + location.host + '/real/href', options.url) + return false + }) + .triggerNative('click') + start() +}) + +asyncTest('the event selector strings are overridable', 1, function() { + ok($.rails.linkClickSelector.indexOf(', a[data-custom-remote-link]') != -1, 'linkClickSelector contains custom selector') + start() +}) + +asyncTest('including rails-ujs multiple times throws error', 1, function() { + throws(function() { + Rails.start() + }, 'appending rails.js again throws error') + setTimeout(function() { start() }, 50) +}) + +})() diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js new file mode 100644 index 0000000000..299c71bb00 --- /dev/null +++ b/actionview/test/ujs/public/test/settings.js @@ -0,0 +1,116 @@ +var App = App || {} + +App.assertCallbackInvoked = function(callbackName) { + ok(true, callbackName + ' callback should have been invoked') +} + +App.assertCallbackNotInvoked = function(callbackName) { + ok(false, callbackName + ' callback should not have been invoked') +} + +App.assertGetRequest = function(requestEnv) { + equal(requestEnv['REQUEST_METHOD'], 'GET', 'request type should be GET') +} + +App.assertPostRequest = function(requestEnv) { + equal(requestEnv['REQUEST_METHOD'], 'POST', 'request type should be POST') +} + +App.assertRequestPath = function(requestEnv, path) { + equal(requestEnv['PATH_INFO'], path, 'request should be sent to right url') +} + +App.getVal = function(el) { + return el.is('input,textarea,select') ? el.val() : el.text() +} + +App.disabled = function(el) { + return el.is('input,textarea,select,button') ? + (el.is(':disabled') && $.rails.getData(el[0], 'ujs:disabled')) : + $.rails.getData(el[0], 'ujs:disabled') +} + +App.checkEnabledState = function(el, text) { + ok(!App.disabled(el), el.get(0).tagName + ' should not be disabled') + equal(App.getVal(el), text, el.get(0).tagName + ' text should be original value') +} + +App.checkDisabledState = function(el, text) { + ok(App.disabled(el), el.get(0).tagName + ' should be disabled') + equal(App.getVal(el), text, el.get(0).tagName + ' text should be disabled value') +} + +// hijacks normal form submit; lets it submit to an iframe to prevent +// navigating away from the test suite +$(document).bind('submit', function(e) { + if (!e.isDefaultPrevented()) { + var form = $(e.target), action = form.attr('action'), + name = 'form-frame' + jQuery.guid++, + iframe = $('<iframe name="' + name + '" />'), + iframeInput = '<input name="iframe" value="true" type="hidden" />' + targetInput = '<input name="_target" value="' + (form.attr('target') || '') + '" type="hidden" />' + + if (action && action.indexOf('iframe') < 0) { + if (action.indexOf('?') < 0) { + form.attr('action', action + '?iframe=true') + } else { + form.attr('action', action + '&iframe=true') + } + } + form.attr('target', name).append(iframeInput, targetInput) + $('#qunit-fixture').append(iframe) + $.event.trigger('iframe:loading', form) + } +}) + +var _MouseEvent = window.MouseEvent + +try { + new _MouseEvent() +} catch (e) { + _MouseEvent = function(type, options) { + var evt = document.createEvent('MouseEvents') + evt.initMouseEvent(type, options.bubbles, options.cancelable, window, options.detail, 0, 0, 80, 20, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, null) + return evt + } +} + +$.fn.extend({ + // trigger a native click event + triggerNative: function(type, options) { + var el = this[0], + event, + Evt = { + 'click': _MouseEvent, + 'change': Event, + 'pageshow': PageTransitionEvent, + 'submit': Event + }[type] + + options = options || {} + options.bubbles = true + options.cancelable = true + + event = new Evt(type, options) + + el.dispatchEvent(event) + + if (type === 'submit' && !event.defaultPrevented) { + el.submit() + } + return this + }, + bindNative: function(event, handler) { + if (!handler) return this + + this.bind(event, function(e) { + var args = [] + if (e.originalEvent.detail) { + args = e.originalEvent.detail.slice() + } + args.unshift(e) + return handler.apply(this, args) + }) + return this + } +}) diff --git a/actionview/test/ujs/public/vendor/jquery.metadata.js b/actionview/test/ujs/public/vendor/jquery.metadata.js new file mode 100644 index 0000000000..5b5253cdf7 --- /dev/null +++ b/actionview/test/ujs/public/vendor/jquery.metadata.js @@ -0,0 +1,122 @@ +/* + * Metadata - jQuery plugin for parsing metadata from elements + * + * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id: jquery.metadata.js 4187 2007-12-16 17:15:27Z joern.zaefferer $ + * + */ + +/** + * Sets the type of metadata to use. Metadata is encoded in JSON, and each property + * in the JSON will become a property of the element itself. + * + * There are three supported types of metadata storage: + * + * attr: Inside an attribute. The name parameter indicates *which* attribute. + * + * class: Inside the class attribute, wrapped in curly braces: { } + * + * elem: Inside a child element (e.g. a script tag). The + * name parameter indicates *which* element. + * + * The metadata for an element is loaded the first time the element is accessed via jQuery. + * + * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements + * matched by expr, then redefine the metadata type and run another $(expr) for other elements. + * + * @name $.metadata.setType + * + * @example <p id="one" class="some_class {item_id: 1, item_label: 'Label'}">This is a p</p> + * @before $.metadata.setType("class") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from the class attribute + * + * @example <p id="one" class="some_class" data="{item_id: 1, item_label: 'Label'}">This is a p</p> + * @before $.metadata.setType("attr", "data") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a "data" attribute + * + * @example <p id="one" class="some_class"><script>{item_id: 1, item_label: 'Label'}</script>This is a p</p> + * @before $.metadata.setType("elem", "script") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a nested script element + * + * @param String type The encoding type + * @param String name The name of the attribute to be used to get metadata (optional) + * @cat Plugins/Metadata + * @descr Sets the type of encoding to be used when loading metadata for the first time + * @type undefined + * @see metadata() + */ + +(function($) { + +$.extend({ + metadata : { + defaults : { + type: 'class', + name: 'metadata', + cre: /({.*})/, + single: 'metadata' + }, + setType: function( type, name ){ + this.defaults.type = type; + this.defaults.name = name; + }, + get: function( elem, opts ){ + var settings = $.extend({},this.defaults,opts); + // check for empty string in single property + if ( !settings.single.length ) settings.single = 'metadata'; + + var data = $.data(elem, settings.single); + // returned cached data if it already exists + if ( data ) return data; + + data = "{}"; + + if ( settings.type == "class" ) { + var m = settings.cre.exec( elem.className ); + if ( m ) + data = m[1]; + } else if ( settings.type == "elem" ) { + if( !elem.getElementsByTagName ) + return undefined; + var e = elem.getElementsByTagName(settings.name); + if ( e.length ) + data = $.trim(e[0].innerHTML); + } else if ( elem.getAttribute != undefined ) { + var attr = elem.getAttribute( settings.name ); + if ( attr ) + data = attr; + } + + if ( data.indexOf( '{' ) <0 ) + data = "{" + data + "}"; + + data = eval("(" + data + ")"); + + $.data( elem, settings.single, data ); + return data; + } + } +}); + +/** + * Returns the metadata object for the first member of the jQuery object. + * + * @name metadata + * @descr Returns element's metadata object + * @param Object opts An object containing settings to override the defaults + * @type jQuery + * @cat Plugins/Metadata + */ +$.fn.metadata = function( opts ){ + return $.metadata.get( this[0], opts ); +}; + +})(jQuery);
\ No newline at end of file diff --git a/actionview/test/ujs/public/vendor/qunit.css b/actionview/test/ujs/public/vendor/qunit.css new file mode 100644 index 0000000000..93026e3ba3 --- /dev/null +++ b/actionview/test/ujs/public/vendor/qunit.css @@ -0,0 +1,237 @@ +/*! + * QUnit 1.14.0 + * http://qunitjs.com/ + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-01-31T16:40Z + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { + margin: 0; + padding: 0; +} + + +/** Header */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699A4; + background-color: #0D3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: 400; + + border-radius: 5px 5px 0 0; +} + +#qunit-header a { + text-decoration: none; + color: #C2CCD1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #FFF; +} + +#qunit-testrunner-toolbar label { + display: inline-block; + padding: 0 0.5em 0 0.1em; +} + +#qunit-banner { + height: 5px; +} + +#qunit-testrunner-toolbar { + padding: 0.5em 0 0.5em 2em; + color: #5E740B; + background-color: #EEE; + overflow: hidden; +} + +#qunit-userAgent { + padding: 0.5em 0 0.5em 2.5em; + background-color: #2B81AF; + color: #FFF; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + +#qunit-modulefilter-container { + float: right; +} + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 0.5em 0.4em 2.5em; + border-bottom: 1px solid #FFF; + list-style-position: inside; +} + +#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { + display: none; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li a { + padding: 0.5em; + color: #C2CCD1; + text-decoration: none; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #FFF; + + border-radius: 5px; +} + +.qunit-collapsed { + display: none; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: 0.2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 0.5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + background-color: #E0F2BE; + color: #374E0C; + text-decoration: none; +} + +#qunit-tests ins { + background-color: #FFCACA; + color: #500; + text-decoration: none; +} + +/*** Test Counts */ + +#qunit-tests b.counts { color: #000; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + padding: 5px; + background-color: #FFF; + border-bottom: none; + list-style-position: inside; +} + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #3C510C; + background-color: #FFF; + border-left: 10px solid #C6E746; +} + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #FFF; + border-left: 10px solid #EE5757; + white-space: pre; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 5px 5px; +} + +#qunit-tests .fail { color: #000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: #008000; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + + +/** Result */ + +#qunit-testresult { + padding: 0.5em 0.5em 0.5em 2.5em; + + color: #2B81AF; + background-color: #D2E0E6; + + border-bottom: 1px solid #FFF; +} +#qunit-testresult .module-name { + font-weight: 700; +} + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; + width: 1000px; + height: 1000px; +} diff --git a/actionview/test/ujs/public/vendor/qunit.js b/actionview/test/ujs/public/vendor/qunit.js new file mode 100644 index 0000000000..0e279fde17 --- /dev/null +++ b/actionview/test/ujs/public/vendor/qunit.js @@ -0,0 +1,2288 @@ +/*! + * QUnit 1.14.0 + * http://qunitjs.com/ + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-01-31T16:40Z + */ + +(function( window ) { + +var QUnit, + assert, + config, + onErrorFnPrev, + testId = 0, + fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + // Keep a local reference to Date (GH-283) + Date = window.Date, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + defined = { + document: typeof window.document !== "undefined", + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; + } + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. + /*jshint newcap: false */ + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[key]; + vals[key] = val === Object(val) ? objectValues(val) : val; + } + } + return vals; + }; + + +// Root QUnit object. +// `QUnit` initialized at top of scope +QUnit = { + + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment ) { + config.currentModule = name; + config.currentModuleTestEnvironment = testEnvironment; + config.modules[name] = true; + }, + + asyncTest: function( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + QUnit.test( testName, expected, callback, true ); + }, + + test: function( testName, expected, callback, async ) { + var test, + nameHtml = "<span class='test-name'>" + escapeText( testName ) + "</span>"; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + if ( config.currentModule ) { + nameHtml = "<span class='module-name'>" + escapeText( config.currentModule ) + "</span>: " + nameHtml; + } + + test = new Test({ + nameHtml: nameHtml, + testName: testName, + expected: expected, + async: async, + callback: callback, + module: config.currentModule, + moduleTestEnvironment: config.currentModuleTestEnvironment, + stack: sourceFromStacktrace( 2 ) + }); + + if ( !validTest( test ) ) { + return; + } + + test.queue(); + }, + + // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. + expect: function( asserts ) { + if (arguments.length === 1) { + config.current.expected = asserts; + } else { + return config.current.expected; + } + }, + + start: function( count ) { + // QUnit hasn't been initialized yet. + // Note: RequireJS (et al) may delay onLoad + if ( config.semaphore === undefined ) { + QUnit.begin(function() { + // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first + setTimeout(function() { + QUnit.start( count ); + }); + }); + return; + } + + config.semaphore -= count || 1; + // don't start until equal number of stop-calls + if ( config.semaphore > 0 ) { + return; + } + // ignore if start is called more often then stop + if ( config.semaphore < 0 ) { + config.semaphore = 0; + QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); + return; + } + // A slight delay, to avoid any current callbacks + if ( defined.setTimeout ) { + setTimeout(function() { + if ( config.semaphore > 0 ) { + return; + } + if ( config.timeout ) { + clearTimeout( config.timeout ); + } + + config.blocking = false; + process( true ); + }, 13); + } else { + config.blocking = false; + process( true ); + } + }, + + stop: function( count ) { + config.semaphore += count || 1; + config.blocking = true; + + if ( config.testTimeout && defined.setTimeout ) { + clearTimeout( config.timeout ); + config.timeout = setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + config.semaphore = 1; + QUnit.start(); + }, config.testTimeout ); + } + } +}; + +// We use the prototype to distinguish between properties that should +// be exposed as globals (and in exports) and those that shouldn't +(function() { + function F() {} + F.prototype = QUnit; + QUnit = new F(); + // Make F QUnit's constructor so that we can add to the prototype later + QUnit.constructor = F; +}()); + +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true, + + // when enabled, show only failing tests + // gets persisted through sessionStorage and can be changed in UI via checkbox + hidepassed: false, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + // by default, scroll to top of the page when suite is done + scrolltop: true, + + // when enabled, all tests must call expect() + requireExpects: false, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: {}, + + // logging callback queues + begin: [], + done: [], + log: [], + testStart: [], + testDone: [], + moduleStart: [], + moduleDone: [] +}; + +// Initialize more QUnit.config and QUnit.urlParams +(function() { + var i, current, + location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + if ( urlParams[ current[ 0 ] ] ) { + urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); + } else { + urlParams[ current[ 0 ] ] = current[ 1 ]; + } + } + } + + QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName + config.filter = urlParams.filter; + + // Exact match of the module name + config.module = urlParams.module; + + config.testNumber = []; + if ( urlParams.testNumber ) { + + // Ensure that urlParams.testNumber is an array + urlParams.testNumber = [].concat( urlParams.testNumber ); + for ( i = 0; i < urlParams.testNumber.length; i++ ) { + current = urlParams.testNumber[ i ]; + config.testNumber.push( parseInt( current, 10 ) ); + } + } + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = location.protocol === "file:"; +}()); + +extend( QUnit, { + + config: config, + + // Initialize the configuration options + init: function() { + extend( config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date(), + updateRate: 1000, + blocking: false, + autostart: true, + autorun: false, + filter: "", + queue: [], + semaphore: 1 + }); + + var tests, banner, result, + qunit = id( "qunit" ); + + if ( qunit ) { + qunit.innerHTML = + "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" + + "<h2 id='qunit-banner'></h2>" + + "<div id='qunit-testrunner-toolbar'></div>" + + "<h2 id='qunit-userAgent'></h2>" + + "<ol id='qunit-tests'></ol>"; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...<br/> "; + } + }, + + // Resets the test setup. Useful for tests that modify the DOM. + /* + DEPRECATED: Use multiple tests instead of resetting inside a test. + Use testStart or testDone for custom cleanup. + This method will throw an error in 2.0, and will be removed in 2.1 + */ + reset: function() { + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + fixture.innerHTML = config.fixture; + } + }, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) === type; + }, + + objectType: function( obj ) { + if ( typeof obj === "undefined" ) { + return "undefined"; + } + + // Consider: typeof null === object + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; + + switch ( type ) { + case "Number": + if ( isNaN(obj) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Date": + case "RegExp": + case "Function": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } + return undefined; + }, + + push: function( result, actual, expected, message ) { + if ( !config.current ) { + throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); + } + + var output, source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeText( message ) || ( result ? "okay" : "failed" ); + message = "<span class='test-message'>" + message + "</span>"; + output = message; + + if ( !result ) { + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); + output += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + expected + "</pre></td></tr>"; + + if ( actual !== expected ) { + output += "<tr class='test-actual'><th>Result: </th><td><pre>" + actual + "</pre></td></tr>"; + output += "<tr class='test-diff'><th>Diff: </th><td><pre>" + QUnit.diff( expected, actual ) + "</pre></td></tr>"; + } + + source = sourceFromStacktrace(); + + if ( source ) { + details.source = source; + output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>"; + } + + output += "</table>"; + } + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + pushFailure: function( message, source, actual ) { + if ( !config.current ) { + throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + + var output, + details = { + module: config.current.module, + name: config.current.testName, + result: false, + message: message + }; + + message = escapeText( message ) || "error"; + message = "<span class='test-message'>" + message + "</span>"; + output = message; + + output += "<table>"; + + if ( actual ) { + output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText( actual ) + "</pre></td></tr>"; + } + + if ( source ) { + details.source = source; + output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>"; + } + + output += "</table>"; + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: false, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var key, + querystring = "?"; + + for ( key in params ) { + if ( hasOwn.call( params, key ) ) { + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } + } + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); + }, + + extend: extend, + id: id, + addEvent: addEvent, + addClass: addClass, + hasClass: hasClass, + removeClass: removeClass + // load, equiv, jsDump, diff: Attached later +}); + +/** + * @deprecated: Created for backwards compatibility with test runner that set the hook function + * into QUnit.{hook}, instead of invoking it and passing the hook function. + * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. + * Doing this allows us to tell if the following methods have been overwritten on the actual + * QUnit object. + */ +extend( QUnit.constructor.prototype, { + + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: registerLoggingCallback( "begin" ), + + // done: { failed, passed, total, runtime } + done: registerLoggingCallback( "done" ), + + // log: { result, actual, expected, message } + log: registerLoggingCallback( "log" ), + + // testStart: { name } + testStart: registerLoggingCallback( "testStart" ), + + // testDone: { name, failed, passed, total, runtime } + testDone: registerLoggingCallback( "testDone" ), + + // moduleStart: { name } + moduleStart: registerLoggingCallback( "moduleStart" ), + + // moduleDone: { name, failed, passed, total } + moduleDone: registerLoggingCallback( "moduleDone" ) +}); + +if ( !defined.document || document.readyState === "complete" ) { + config.autorun = true; +} + +QUnit.load = function() { + runLoggingCallbacks( "begin", QUnit, {} ); + + // Initialize the config, saving the execution queue + var banner, filter, i, j, label, len, main, ol, toolbar, val, selection, + urlConfigContainer, moduleFilter, userAgent, + numModules = 0, + moduleNames = [], + moduleFilterHtml = "", + urlConfigHtml = "", + oldconfig = extend( {}, config ); + + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + len = config.urlConfig.length; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[i]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val + }; + } + config[ val.id ] = QUnit.urlParams[ val.id ]; + if ( !val.value || typeof val.value === "string" ) { + urlConfigHtml += "<input id='qunit-urlconfig-" + escapeText( val.id ) + + "' name='" + escapeText( val.id ) + + "' type='checkbox'" + + ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) + + ( config[ val.id ] ? " checked='checked'" : "" ) + + " title='" + escapeText( val.tooltip ) + + "'><label for='qunit-urlconfig-" + escapeText( val.id ) + + "' title='" + escapeText( val.tooltip ) + "'>" + val.label + "</label>"; + } else { + urlConfigHtml += "<label for='qunit-urlconfig-" + escapeText( val.id ) + + "' title='" + escapeText( val.tooltip ) + + "'>" + val.label + + ": </label><select id='qunit-urlconfig-" + escapeText( val.id ) + + "' name='" + escapeText( val.id ) + + "' title='" + escapeText( val.tooltip ) + + "'><option></option>"; + selection = false; + if ( QUnit.is( "array", val.value ) ) { + for ( j = 0; j < val.value.length; j++ ) { + urlConfigHtml += "<option value='" + escapeText( val.value[j] ) + "'" + + ( config[ val.id ] === val.value[j] ? + (selection = true) && " selected='selected'" : + "" ) + + ">" + escapeText( val.value[j] ) + "</option>"; + } + } else { + for ( j in val.value ) { + if ( hasOwn.call( val.value, j ) ) { + urlConfigHtml += "<option value='" + escapeText( j ) + "'" + + ( config[ val.id ] === j ? + (selection = true) && " selected='selected'" : + "" ) + + ">" + escapeText( val.value[j] ) + "</option>"; + } + } + } + if ( config[ val.id ] && !selection ) { + urlConfigHtml += "<option value='" + escapeText( config[ val.id ] ) + + "' selected='selected' disabled='disabled'>" + + escapeText( config[ val.id ] ) + + "</option>"; + } + urlConfigHtml += "</select>"; + } + } + for ( i in config.modules ) { + if ( config.modules.hasOwnProperty( i ) ) { + moduleNames.push(i); + } + } + numModules = moduleNames.length; + moduleNames.sort( function( a, b ) { + return a.localeCompare( b ); + }); + moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " + + ( config.module === undefined ? "selected='selected'" : "" ) + + ">< All Modules ></option>"; + + + for ( i = 0; i < numModules; i++) { + moduleFilterHtml += "<option value='" + escapeText( encodeURIComponent(moduleNames[i]) ) + "' " + + ( config.module === moduleNames[i] ? "selected='selected'" : "" ) + + ">" + escapeText(moduleNames[i]) + "</option>"; + } + moduleFilterHtml += "</select>"; + + // `userAgent` initialized at top of scope + userAgent = id( "qunit-userAgent" ); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + + // `banner` initialized at top of scope + banner = id( "qunit-header" ); + if ( banner ) { + banner.innerHTML = "<a href='" + QUnit.url({ filter: undefined, module: undefined, testNumber: undefined }) + "'>" + banner.innerHTML + "</a> "; + } + + // `toolbar` initialized at top of scope + toolbar = id( "qunit-testrunner-toolbar" ); + if ( toolbar ) { + // `filter` initialized at top of scope + filter = document.createElement( "input" ); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + + addEvent( filter, "click", function() { + var tmp, + ol = id( "qunit-tests" ); + + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace( / hidepass /, " " ); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); + } else { + sessionStorage.removeItem( "qunit-filter-passed-tests" ); + } + } + }); + + if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { + filter.checked = true; + // `ol` initialized at top of scope + ol = id( "qunit-tests" ); + ol.className = ol.className + " hidepass"; + } + toolbar.appendChild( filter ); + + // `label` initialized at top of scope + label = document.createElement( "label" ); + label.setAttribute( "for", "qunit-filter-pass" ); + label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + + urlConfigContainer = document.createElement("span"); + urlConfigContainer.innerHTML = urlConfigHtml; + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" for checkboxes + // * Fallback from event.target to event.srcElement + addEvents( urlConfigContainer.getElementsByTagName("input"), "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? + target.defaultValue || true : + undefined; + window.location = QUnit.url( params ); + }); + addEvents( urlConfigContainer.getElementsByTagName("select"), "change", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.options[ target.selectedIndex ].value || undefined; + window.location = QUnit.url( params ); + }); + toolbar.appendChild( urlConfigContainer ); + + if (numModules > 1) { + moduleFilter = document.createElement( "span" ); + moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); + moduleFilter.innerHTML = moduleFilterHtml; + addEvent( moduleFilter.lastChild, "change", function() { + var selectBox = moduleFilter.getElementsByTagName("select")[0], + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + + window.location = QUnit.url({ + module: ( selectedModule === "" ) ? undefined : selectedModule, + // Remove any existing filters + filter: undefined, + testNumber: undefined + }); + }); + toolbar.appendChild(moduleFilter); + } + } + + // `main` initialized at top of scope + main = id( "qunit-fixture" ); + if ( main ) { + config.fixture = main.innerHTML; + } + + if ( config.autostart ) { + QUnit.start(); + } +}; + +if ( defined.document ) { + addEvent( window, "load", QUnit.load ); +} + +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will suppress the default browser handler, +// returning false will let it run. +window.onerror = function ( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend( function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: validTest } ) ); + } + return false; + } + + return ret; +}; + +function done() { + config.autorun = true; + + // Log the last module results + if ( config.previousModule ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + delete config.previousModule; + + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + runtime = +new Date() - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + "Tests completed in ", + runtime, + " milliseconds.<br/>", + "<span class='passed'>", + passed, + "</span> assertions of <span class='total'>", + config.stats.all, + "</span> passed, <span class='failed'>", + config.stats.bad, + "</span> failed." + ].join( "" ); + + if ( banner ) { + banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( config.altertitle && defined.document && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( config.stats.bad ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { + // `key` & `i` initialized at top of scope + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } + } + } + + // scroll back to top to show results + if ( config.scrolltop && window.scrollTo ) { + window.scrollTo(0, 0); + } + + runLoggingCallbacks( "done", QUnit, { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); +} + +/** @return Boolean: true if this test should be ran */ +function validTest( test ) { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = config.module && config.module.toLowerCase(), + fullName = ( test.module + ": " + test.testName ).toLowerCase(); + + // Internally-generated tests are always valid + if ( test.callback && test.callback.validTest === validTest ) { + delete test.callback.validTest; + return true; + } + + if ( config.testNumber.length > 0 ) { + if ( inArray( test.testNumber, config.testNumber ) < 0 ) { + return false; + } + } + + if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { + return false; + } + + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; + } + + // Otherwise, do the opposite + return !include; +} + +// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) +// Later Safari and IE10 are supposed to support error.stack as well +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 3 : offset; + + var stack, include, i; + + if ( e.stacktrace ) { + // Opera + return e.stacktrace.split( "\n" )[ offset + 3 ]; + } else if ( e.stack ) { + // Firefox, Chrome + stack = e.stack.split( "\n" ); + if (/^error$/i.test( stack[0] ) ) { + stack.shift(); + } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } + return stack[ offset ]; + } else if ( e.sourceURL ) { + // Safari, PhantomJS + // hopefully one day Safari provides actual stacktraces + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} +function sourceFromStacktrace( offset ) { + try { + throw new Error(); + } catch ( e ) { + return extractStacktrace( e, offset ); + } +} + +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { + if ( !s ) { + return ""; + } + s = s + ""; + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch( s ) { + case "'": + return "'"; + case "\"": + return """; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + } + }); +} + +function synchronize( callback, last ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +function process( last ) { + function next() { + process( last ); + } + var start = new Date().getTime(); + config.depth = config.depth ? config.depth + 1 : 1; + + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { + config.queue.shift()(); + } else { + setTimeout( next, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + if ( hasOwn.call( window, key ) ) { + // in Opera sometimes DOM element ids show up here, ignore them + if ( /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } + } + } +} + +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + + saveGlobal(); + + newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); + } + + deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice( i, 1 ); + i--; + break; + } + } + } + return result; +} + +function extend( a, b ) { + for ( var prop in b ) { + if ( hasOwn.call( b, prop ) ) { + // Avoid "Member not found" error in IE8 caused by messing with window.constructor + if ( !( prop === "constructor" && a === window ) ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else { + a[ prop ] = b[ prop ]; + } + } + } + } + + return a; +} + +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + if ( elem.addEventListener ) { + + // Standards-based browsers + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + + // support: IE <9 + elem.attachEvent( "on" + type, fn ); + } else { + + // Caller must ensure support for event listeners is present + throw new Error( "addEvent() was called in a context without event listener support" ); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); + } + // If possible, trim it for prettiness, but not necessarily + elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); +} + +function id( name ) { + return defined.document && document.getElementById && document.getElementById( name ); +} + +function registerLoggingCallback( key ) { + return function( callback ) { + config[key].push( callback ); + }; +} + +// Supports deprecated method of completely overwriting logging callbacks +function runLoggingCallbacks( key, scope, args ) { + var i, callbacks; + if ( QUnit.hasOwnProperty( key ) ) { + QUnit[ key ].call(scope, args ); + } else { + callbacks = config[ key ]; + for ( i = 0; i < callbacks.length; i++ ) { + callbacks[ i ].call( scope, args ); + } + } +} + +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +function Test( settings ) { + extend( this, settings ); + this.assertions = []; + this.testNumber = ++Test.count; +} + +Test.count = 0; + +Test.prototype = { + init: function() { + var a, b, li, + tests = id( "qunit-tests" ); + + if ( tests ) { + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml; + + // `a` initialized at top of scope + a = document.createElement( "a" ); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ testNumber: this.testNumber }); + + li = document.createElement( "li" ); + li.appendChild( b ); + li.appendChild( a ); + li.className = "running"; + li.id = this.id = "qunit-test-output" + testId++; + + tests.appendChild( li ); + } + }, + setup: function() { + if ( + // Emit moduleStart when we're switching from one module to another + this.module !== config.previousModule || + // They could be equal (both undefined) but if the previousModule property doesn't + // yet exist it means this is the first test in a suite that isn't wrapped in a + // module, in which case we'll just emit a moduleStart event for 'undefined'. + // Without this, reporters can get testStart before moduleStart which is a problem. + !hasOwn.call( config, "previousModule" ) + ) { + if ( hasOwn.call( config, "previousModule" ) ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + config.previousModule = this.module; + config.moduleStats = { all: 0, bad: 0 }; + runLoggingCallbacks( "moduleStart", QUnit, { + name: this.module + }); + } + + config.current = this; + + this.testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, this.moduleTestEnvironment ); + + this.started = +new Date(); + runLoggingCallbacks( "testStart", QUnit, { + name: this.testName, + module: this.module + }); + + /*jshint camelcase:false */ + + + /** + * Expose the current test environment. + * + * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. + */ + QUnit.current_testEnvironment = this.testEnvironment; + + /*jshint camelcase:true */ + + if ( !config.pollution ) { + saveGlobal(); + } + if ( config.notrycatch ) { + this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); + return; + } + try { + this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); + } catch( e ) { + QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + }, + run: function() { + config.current = this; + + var running = id( "qunit-testresult" ); + + if ( running ) { + running.innerHTML = "Running: <br/>" + this.nameHtml; + } + + if ( this.async ) { + QUnit.stop(); + } + + this.callbackStarted = +new Date(); + + if ( config.notrycatch ) { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + return; + } + + try { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + } catch( e ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + + QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + QUnit.start(); + } + } + }, + teardown: function() { + config.current = this; + if ( config.notrycatch ) { + if ( typeof this.callbackRuntime === "undefined" ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + } + this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); + return; + } else { + try { + this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); + } catch( e ) { + QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + } + checkPollution(); + }, + finish: function() { + config.current = this; + if ( config.requireExpects && this.expected === null ) { + QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { + QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); + } else if ( this.expected === null && !this.assertions.length ) { + QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); + } + + var i, assertion, a, b, time, li, ol, + test = this, + good = 0, + bad = 0, + tests = id( "qunit-tests" ); + + this.runtime = +new Date() - this.started; + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + if ( tests ) { + ol = document.createElement( "ol" ); + ol.className = "qunit-assert-list"; + + for ( i = 0; i < this.assertions.length; i++ ) { + assertion = this.assertions[i]; + + li = document.createElement( "li" ); + li.className = assertion.result ? "pass" : "fail"; + li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + // store result when possible + if ( QUnit.config.reorder && defined.sessionStorage ) { + if ( bad ) { + sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); + } else { + sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); + } + } + + if ( bad === 0 ) { + addClass( ol, "qunit-collapsed" ); + } + + // `b` initialized at top of scope + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>"; + + addEvent(b, "click", function() { + var next = b.parentNode.lastChild, + collapsed = hasClass( next, "qunit-collapsed" ); + ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); + }); + + addEvent(b, "dblclick", function( e ) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { + target = target.parentNode; + } + if ( window.location && target.nodeName.toLowerCase() === "strong" ) { + window.location = QUnit.url({ testNumber: test.testNumber }); + } + }); + + // `time` initialized at top of scope + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = this.runtime + " ms"; + + // `li` initialized at top of scope + li = id( this.id ); + li.className = bad ? "fail" : "pass"; + li.removeChild( li.firstChild ); + a = li.firstChild; + li.appendChild( b ); + li.appendChild( a ); + li.appendChild( time ); + li.appendChild( ol ); + + } else { + for ( i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + runLoggingCallbacks( "testDone", QUnit, { + name: this.testName, + module: this.module, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length, + runtime: this.runtime, + // DEPRECATED: this property will be removed in 2.0.0, use runtime instead + duration: this.runtime + }); + + QUnit.reset(); + + config.current = undefined; + }, + + queue: function() { + var bad, + test = this; + + synchronize(function() { + test.init(); + }); + function run() { + // each of these can by async + synchronize(function() { + test.setup(); + }); + synchronize(function() { + test.run(); + }); + synchronize(function() { + test.teardown(); + }); + synchronize(function() { + test.finish(); + }); + } + + // `bad` initialized at top of scope + // defer when previous test run passed, if storage is available + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); + + if ( bad ) { + run(); + } else { + synchronize( run, true ); + } + } +}; + +// `assert` initialized at top of scope +// Assert helpers +// All of these must either call QUnit.push() or manually do: +// - runLoggingCallbacks( "log", .. ); +// - config.current.assertions.push({ .. }); +assert = QUnit.assert = { + /** + * Asserts rough true-ish result. + * @name ok + * @function + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function( result, msg ) { + if ( !config.current ) { + throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + result = !!result; + msg = msg || ( result ? "okay" : "failed" ); + + var source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: msg + }; + + msg = "<span class='test-message'>" + escapeText( msg ) + "</span>"; + + if ( !result ) { + source = sourceFromStacktrace( 2 ); + if ( source ) { + details.source = source; + msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" + + escapeText( source ) + + "</pre></td></tr></table>"; + } + } + runLoggingCallbacks( "log", QUnit, details ); + config.current.assertions.push({ + result: result, + message: msg + }); + }, + + /** + * Assert that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * @name equal + * @function + * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); + */ + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected == actual, actual, expected, message ); + }, + + /** + * @name notEqual + * @function + */ + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected != actual, actual, expected, message ); + }, + + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name deepEqual + * @function + */ + deepEqual: function( actual, expected, message ) { + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notDeepEqual + * @function + */ + notDeepEqual: function( actual, expected, message ) { + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name strictEqual + * @function + */ + strictEqual: function( actual, expected, message ) { + QUnit.push( expected === actual, actual, expected, message ); + }, + + /** + * @name notStrictEqual + * @function + */ + notStrictEqual: function( actual, expected, message ) { + QUnit.push( expected !== actual, actual, expected, message ); + }, + + "throws": function( block, expected, message ) { + var actual, + expectedOutput = expected, + ok = false; + + // 'expected' is optional + if ( !message && typeof expected === "string" ) { + message = expected; + expected = null; + } + + config.current.ignoreGlobalErrors = true; + try { + block.call( config.current.testEnvironment ); + } catch (e) { + actual = e; + } + config.current.ignoreGlobalErrors = false; + + if ( actual ) { + + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + + // expected is an Error object + } else if ( expected instanceof Error ) { + ok = actual instanceof Error && + actual.name === expected.name && + actual.message === expected.message; + + // expected is a regexp + } else if ( QUnit.objectType( expected ) === "regexp" ) { + ok = expected.test( errorString( actual ) ); + + // expected is a string + } else if ( QUnit.objectType( expected ) === "string" ) { + ok = expected === errorString( actual ); + + // expected is a constructor + } else if ( actual instanceof expected ) { + ok = true; + + // expected is a validation function which returns true is validation passed + } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + + QUnit.push( ok, actual, expectedOutput, message ); + } else { + QUnit.pushFailure( message, null, "No exception was thrown." ); + } + } +}; + +/** + * @deprecated since 1.8.0 + * Kept assertion helpers in root for backwards compatibility. + */ +extend( QUnit.constructor.prototype, assert ); + +/** + * @deprecated since 1.9.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.constructor.prototype.raises = function() { + QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" ); +}; + +/** + * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.constructor.prototype.equals = function() { + QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); +}; +QUnit.constructor.prototype.same = function() { + QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); +}; + +// Test for equality any JavaScript type. +// Author: Philippe Rathé <prathe@gmail.com> +QUnit.equiv = (function() { + + // Call the o related callback with the given arguments. + function bindCallbacks( o, callbacks, args ) { + var prop = QUnit.objectType( o ); + if ( prop ) { + if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { + return callbacks[ prop ].apply( callbacks, args ); + } else { + return callbacks[ prop ]; // or undefined + } + } + } + + // the real equiv function + var innerEquiv, + // stack to decide between skip/abort functions + callers = [], + // stack to avoiding loops from circular referencing + parents = [], + parentsB = [], + + getProto = Object.getPrototypeOf || function ( obj ) { + /*jshint camelcase:false */ + return obj.__proto__; + }, + callbacks = (function () { + + // for string, boolean, number and null + function useStrictEquality( b, a ) { + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { + // to catch short annotation VS 'new' annotation of a + // declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function( b ) { + return isNaN( b ); + }, + + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && + // the regex itself + a.source === b.source && + // and its modifiers + a.global === b.global && + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[callers.length - 1]; + return caller !== Object && typeof caller !== "undefined"; + }, + + "array": function( b, a ) { + var i, j, len, loop, aCircular, bCircular; + + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } + + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[j] === a[i]; + bCircular = parentsB[j] === b[i]; + if ( aCircular || bCircular ) { + if ( a[i] === b[i] || aCircular && bCircular ) { + loop = true; + } else { + parents.pop(); + parentsB.pop(); + return false; + } + } + } + if ( !loop && !innerEquiv(a[i], b[i]) ) { + parents.pop(); + parentsB.pop(); + return false; + } + } + parents.pop(); + parentsB.pop(); + return true; + }, + + "object": function( b, a ) { + /*jshint forin:false */ + var i, j, loop, aCircular, bCircular, + // Default to true + eq = true, + aProperties = [], + bProperties = []; + + // comparing constructors is more strict than using + // instanceof + if ( a.constructor !== b.constructor ) { + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || + ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { + return false; + } + } + + // stack constructor before traversing properties + callers.push( a.constructor ); + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + + // be strict: don't ensure hasOwnProperty and go deep + for ( i in a ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[j] === a[i]; + bCircular = parentsB[j] === b[i]; + if ( aCircular || bCircular ) { + if ( a[i] === b[i] || aCircular && bCircular ) { + loop = true; + } else { + eq = false; + break; + } + } + } + aProperties.push(i); + if ( !loop && !innerEquiv(a[i], b[i]) ) { + eq = false; + break; + } + } + + parents.pop(); + parentsB.pop(); + callers.pop(); // unstack, we are done + + for ( i in b ) { + bProperties.push( i ); // collect b's properties + } + + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + }()); + + innerEquiv = function() { // can take multiple arguments + var args = [].slice.apply( arguments ); + if ( args.length < 2 ) { + return true; // end transition + } + + return (function( a, b ) { + if ( a === b ) { + return true; // catch the most you can + } else if ( a === null || b === null || typeof a === "undefined" || + typeof b === "undefined" || + QUnit.objectType(a) !== QUnit.objectType(b) ) { + return false; // don't lose time with error prone cases + } else { + return bindCallbacks(a, callbacks, [ b, a ]); + } + + // apply transition with (1..n) arguments + }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); + }; + + return innerEquiv; +}()); + +/** + * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | + * http://flesler.blogspot.com Licensed under BSD + * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 + * + * @projectDescription Advanced and extensible data dumping for Javascript. + * @version 1.0.0 + * @author Ariel Flesler + * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} + */ +QUnit.jsDump = (function() { + function quote( str ) { + return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; + } + function literal( o ) { + return o + ""; + } + function join( pre, arr, post ) { + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if ( arr.join ) { + arr = arr.join( "," + s + inner ); + } + if ( !arr ) { + return pre + post; + } + return [ pre, inner + arr, base + post ].join(s); + } + function array( arr, stack ) { + var i = arr.length, ret = new Array(i); + this.up(); + while ( i-- ) { + ret[i] = this.parse( arr[i] , undefined , stack); + } + this.down(); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + jsDump = { + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { + stack = stack || [ ]; + var inStack, res, + parser = this.parsers[ type || this.typeOf(obj) ]; + + type = typeof parser; + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + (inStack - stack.length) + ")"; + } + if ( type === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( type === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj) ) { + type = "date"; + } else if ( QUnit.is( "function", obj) ) { + type = "function"; + } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + // native arrays + toString.call( obj ) === "[object Array]" || + // NodeList objects + ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; + }, + separator: function() { + return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " "; + }, + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); + } + return new Array( this.depth + ( extra || 0 ) ).join(chr); + }, + up: function( a ) { + this.depth += a || 1; + }, + down: function( a ) { + this.depth -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + depth: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + /*jshint forin:false */ + var ret = [ ], keys, key, val, i; + QUnit.jsDump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); + } + QUnit.jsDump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = QUnit.jsDump.HTML ? "<" : "<", + close = QUnit.jsDump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } + } + } + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } + + args = new Array(l); + while ( l-- ) { + // 97 is 'a' + args[l] = String.fromCharCode(97+l); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; + + return jsDump; +}()); + +/* + * Javascript Diff Algorithm + * By John Resig (http://ejohn.org/) + * Modified by Chu Alan "sprite" + * + * Released under the MIT license. + * + * More Info: + * http://ejohn.org/projects/javascript-diff-algorithm/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over" + */ +QUnit.diff = (function() { + /*jshint eqeqeq:false, eqnull:true */ + function diff( o, n ) { + var i, + ns = {}, + os = {}; + + for ( i = 0; i < n.length; i++ ) { + if ( !hasOwn.call( ns, n[i] ) ) { + ns[ n[i] ] = { + rows: [], + o: null + }; + } + ns[ n[i] ].rows.push( i ); + } + + for ( i = 0; i < o.length; i++ ) { + if ( !hasOwn.call( os, o[i] ) ) { + os[ o[i] ] = { + rows: [], + n: null + }; + } + os[ o[i] ].rows.push( i ); + } + + for ( i in ns ) { + if ( hasOwn.call( ns, i ) ) { + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { + n[ ns[i].rows[0] ] = { + text: n[ ns[i].rows[0] ], + row: os[i].rows[0] + }; + o[ os[i].rows[0] ] = { + text: o[ os[i].rows[0] ], + row: ns[i].rows[0] + }; + } + } + } + + for ( i = 0; i < n.length - 1; i++ ) { + if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && + n[ i + 1 ] == o[ n[i].row + 1 ] ) { + + n[ i + 1 ] = { + text: n[ i + 1 ], + row: n[i].row + 1 + }; + o[ n[i].row + 1 ] = { + text: o[ n[i].row + 1 ], + row: i + 1 + }; + } + } + + for ( i = n.length - 1; i > 0; i-- ) { + if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && + n[ i - 1 ] == o[ n[i].row - 1 ]) { + + n[ i - 1 ] = { + text: n[ i - 1 ], + row: n[i].row - 1 + }; + o[ n[i].row - 1 ] = { + text: o[ n[i].row - 1 ], + row: i - 1 + }; + } + } + + return { + o: o, + n: n + }; + } + + return function( o, n ) { + o = o.replace( /\s+$/, "" ); + n = n.replace( /\s+$/, "" ); + + var i, pre, + str = "", + out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), + oSpace = o.match(/\s+/g), + nSpace = n.match(/\s+/g); + + if ( oSpace == null ) { + oSpace = [ " " ]; + } + else { + oSpace.push( " " ); + } + + if ( nSpace == null ) { + nSpace = [ " " ]; + } + else { + nSpace.push( " " ); + } + + if ( out.n.length === 0 ) { + for ( i = 0; i < out.o.length; i++ ) { + str += "<del>" + out.o[i] + oSpace[i] + "</del>"; + } + } + else { + if ( out.n[0].text == null ) { + for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { + str += "<del>" + out.o[n] + oSpace[n] + "</del>"; + } + } + + for ( i = 0; i < out.n.length; i++ ) { + if (out.n[i].text == null) { + str += "<ins>" + out.n[i] + nSpace[i] + "</ins>"; + } + else { + // `pre` initialized at top of scope + pre = ""; + + for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { + pre += "<del>" + out.o[n] + oSpace[n] + "</del>"; + } + str += " " + out.n[i].text + nSpace[i] + pre; + } + } + } + + return str; + }; +}()); + +// For browser, export only select globals +if ( typeof window !== "undefined" ) { + extend( window, QUnit.constructor.prototype ); + window.QUnit = QUnit; +} + +// For CommonJS environments, export everything +if ( typeof module !== "undefined" && module.exports ) { + module.exports = QUnit; +} + + +// Get a reference to the global object, like window in browsers +}( (function() { + return this; +})() )); diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb new file mode 100644 index 0000000000..7deb208af0 --- /dev/null +++ b/actionview/test/ujs/server.rb @@ -0,0 +1,72 @@ +require "rack" +require "rails" +require "action_controller/railtie" +require "action_view/railtie" +require "blade" +require "json" + +module UJS + class Server < Rails::Application + routes.append do + get "/rails-ujs.js" => Blade::Assets.environment + get "/" => "tests#index" + match "/echo" => "tests#echo", via: :all + get "/error" => proc { |env| [403, {}, []] } + end + + config.cache_classes = false + config.eager_load = false + config.secret_key_base = "59d7a4dbd349fa3838d79e330e39690fc22b931e7dc17d9162f03d633d526fbb92dfdb2dc9804c8be3e199631b9c1fbe43fc3e4fc75730b515851849c728d5c7" + config.paths["app/views"].unshift("#{Rails.root}/views") + config.public_file_server.enabled = true + config.logger = Logger.new(STDOUT) + config.log_level = :error + end +end + +module TestsHelper + def test_to(*names) + names = ["/vendor/qunit.js", "settings"] + names + names.map { |name| script_tag name }.join("\n").html_safe + end + + def script_tag(src) + src = "/test/#{src}.js" unless src.index("/") + %(<script src="#{src}" type="text/javascript"></script>).html_safe + end +end + +class TestsController < ActionController::Base + helper TestsHelper + layout "application" + + def index + render :index + end + + def echo + data = { params: params.to_unsafe_h }.update(request.env) + + if params[:content_type] && params[:content] + render inline: params[:content], content_type: params[:content_type] + elsif request.xhr? + render json: JSON.generate(data) + elsif params[:iframe] + payload = JSON.generate(data).gsub("<", "<").gsub(">", ">") + html = <<-HTML + <script> + if (window.top && window.top !== window) + window.top.jQuery.event.trigger('iframe:loaded', #{payload}) + </script> + <p>You shouldn't be seeing this. <a href="#{request.env['HTTP_REFERER']}">Go back</a></p> + HTML + + render html: html.html_safe + else + render plain: "ERROR: #{request.path} requested without ajax", status: 404 + end + end +end + +Blade.initialize! +UJS::Server.initialize! diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb new file mode 100644 index 0000000000..a69cd2d739 --- /dev/null +++ b/actionview/test/ujs/views/layouts/application.html.erb @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html id="html"> + <head> + <title><%= @title %></title> + <link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" /> + <%= script_tag "http://code.jquery.com/jquery-2.2.0.js" %> + <script> + // This is for test in override.js. + // Must go before rails-ujs. + document.addEventListener('rails:attachBindings', function() { + window.Rails.linkClickSelector += ', a[data-custom-remote-link]'; + // Hijacks link click before ujs binds any handlers + // This is only used for ctrl-clicking test on remote links + window.Rails.delegate(document, '#qunit-fixture a', 'click', function(e) { + e.preventDefault(); + }); + }); + </script> + <%= script_tag "/rails-ujs.js" %> + </head> + + <body id="body"> + <%= yield %> + </body> +</html> diff --git a/actionview/test/ujs/views/tests/index.html.erb b/actionview/test/ujs/views/tests/index.html.erb new file mode 100644 index 0000000000..8de6cd0695 --- /dev/null +++ b/actionview/test/ujs/views/tests/index.html.erb @@ -0,0 +1,11 @@ +<% @title = "rails-ujs test" %> + +<%= test_to 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh', 'csrf-token' %> + +<h1 id="qunit-header"><%= @title %></h1> +<h2 id="qunit-banner"></h2> +<div id="qunit-testrunner-toolbar"></div> +<h2 id="qunit-userAgent"></h2> +<ol id="qunit-tests"></ol> + +<div id="qunit-fixture"></div> diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 9bf397af14..77dfdefc05 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,33 +1,8 @@ -* Added instance variable `@queue` to JobWrapper. +* Change logging instrumentation to log errors when a job raises an exception. - This will fix issues in [resque-scheduler](https://github.com/resque/resque-scheduler) `#job_to_hash` method, - so we can use `#enqueue_delayed_selection`, `#remove_delayed` method in resque-scheduler smoothly. + Fixes #26848. - *mu29* + *Steven Bull* -* Yield the job instance so you have access to things like `job.arguments` on the custom logic after retries fail. - *DHH* - -* Added declarative exception handling via `ActiveJob::Base.retry_on` and `ActiveJob::Base.discard_on`. - - Examples: - - class RemoteServiceJob < ActiveJob::Base - retry_on CustomAppException # defaults to 3s wait, 5 attempts - retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 } - retry_on ActiveRecord::StatementInvalid, wait: 5.seconds, attempts: 3 - retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10 - discard_on ActiveJob::DeserializationError - - def perform(*args) - # Might raise CustomAppException or AnotherCustomAppException for something domain specific - # Might raise ActiveRecord::StatementInvalid when a local db deadlock is detected - # Might raise Net::OpenTimeout when the remote service is down - end - end - - *DHH* - - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activejob/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE index a3ffb46b3f..daa726b9f0 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2016 David Heinemeier Hansson +Copyright (c) 2014-2017 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/Rakefile b/activejob/Rakefile index 3953116061..dd03ab0b8f 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 ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) -ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION) +ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION) task default: :test task test: "test:default" @@ -43,9 +43,8 @@ namespace :test do namespace :isolated do task adapter => "test:env:#{adapter}" do - dir = File.dirname(__FILE__) - Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file| - sh(Gem.ruby, "-w", "-I#{dir}/lib", "-I#{dir}/test", file) + Dir.glob("#{__dir__}/test/cases/**/*_test.rb").all? do |file| + sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file) end || raise("Failures") end end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index 2547e91262..2f2b94a4c4 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/activejob/bin/test b/activejob/bin/test new file mode 100755 index 0000000000..a7beb14b27 --- /dev/null +++ b/activejob/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require File.expand_path("../tools/test", COMPONENT_ROOT) diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 20ca7085c6..8b7aef65a2 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2014-2016 David Heinemeier Hansson +# Copyright (c) 2014-2017 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/arguments.rb b/activejob/lib/active_job/arguments.rb index 41ce5f863b..523a0e7f33 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -5,22 +5,10 @@ module ActiveJob # # Wraps the original exception raised as +cause+. class DeserializationError < StandardError - def initialize(e = nil) #:nodoc: - if e - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize #:nodoc: super("Error while trying to deserialize arguments: #{$!.message}") set_backtrace $!.backtrace end - - # The original exception that was raised during deserialization of job - # arguments. - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end # Raised when an unsupported argument type is set as a job argument. We @@ -34,8 +22,8 @@ module ActiveJob module Arguments extend self # :nodoc: - # Calls #uniq since Integer, Fixnum, and Bignum are all the same class on Ruby 2.4+ - TYPE_WHITELIST = [ NilClass, String, Integer, Fixnum, Bignum, Float, BigDecimal, TrueClass, FalseClass ].uniq + TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] + TYPE_WHITELIST.push(Fixnum, Bignum) unless 1.class == Integer # Serializes a set of arguments. Whitelisted types are returned # as-is. Arrays/Hashes are serialized element by element. diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index d5b17de8b5..9aebc880a5 100644 --- a/activejob/lib/active_job/callbacks.rb +++ b/activejob/lib/active_job/callbacks.rb @@ -4,7 +4,7 @@ module ActiveJob # = Active Job Callbacks # # Active Job provides hooks during the life cycle of a job. Callbacks allow you - # to trigger logic during the life cycle of a job. Available callbacks are: + # to trigger logic during this cycle. Available callbacks are: # # * <tt>before_enqueue</tt> # * <tt>around_enqueue</tt> @@ -13,6 +13,8 @@ module ActiveJob # * <tt>around_perform</tt> # * <tt>after_perform</tt> # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks extend ActiveSupport::Concern include ActiveSupport::Callbacks diff --git a/activejob/lib/active_job/configured_job.rb b/activejob/lib/active_job/configured_job.rb index 979280b910..2ff31f2dae 100644 --- a/activejob/lib/active_job/configured_job.rb +++ b/activejob/lib/active_job/configured_job.rb @@ -1,6 +1,6 @@ module ActiveJob class ConfiguredJob #:nodoc: - def initialize(job_class, options={}) + def initialize(job_class, options = {}) @options = options @job_class = job_class end diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index a338061766..548ec89ee2 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -59,7 +59,7 @@ module ActiveJob # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last) # VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last) # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last) - def set(options={}) + def set(options = {}) ConfiguredJob.new(self, options) end end diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index 18051a7d65..c73117e7f3 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -18,8 +18,8 @@ module ActiveJob job_or_instantiate(*args).enqueue end - protected - def job_or_instantiate(*args) + private + def job_or_instantiate(*args) # :doc: args.first.is_a?(self) ? args.first : new(*args) end end @@ -39,7 +39,7 @@ module ActiveJob # my_job_instance.enqueue queue: :important # my_job_instance.enqueue wait_until: Date.tomorrow.midnight # my_job_instance.enqueue priority: 10 - def enqueue(options={}) + def enqueue(options = {}) self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait] self.scheduled_at = options[:wait_until].to_f if options[:wait_until] self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue] diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index d236f03d53..c1b5d35313 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -17,7 +17,7 @@ module ActiveJob # ==== Options # * <tt>:wait</tt> - Re-enqueues the job with a delay specified either in seconds (default: 3 seconds), # as a computing proc that the number of executions so far as an argument, or as a symbol reference of - # <tt>:exponentially_longer<>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt> + # <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt> # (first wait 3s, then 18s, then 83s, etc) # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts) # * <tt>:queue</tt> - Re-enqueues the job on a different queue @@ -104,7 +104,7 @@ module ActiveJob def determine_delay(seconds_or_duration_or_algorithm) case seconds_or_duration_or_algorithm when :exponentially_longer - (executions ** 4) + 2 + (executions**4) + 2 when ActiveSupport::Duration duration = seconds_or_duration_or_algorithm duration.to_i diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 0d50c27938..bf81f37e81 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -6,7 +6,7 @@ module ActiveJob module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index aa97ab2e22..f46d5c68a8 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -69,14 +69,21 @@ module ActiveJob def perform_start(event) info do job = event.payload[:job] - "Performing #{job.class.name} from #{queue_name(event)}" + args_info(job) + "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job) end end def perform(event) - info do - job = event.payload[:job] - "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2)}ms" + job = event.payload[:job] + ex = event.payload[:exception_object] + if ex + error do + "Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(ex.backtrace).join("\n") + end + else + info do + "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms" + end end end diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index 7f9a2da4b0..b22d8b8347 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -1,5 +1,3 @@ -require "active_job/queue_adapters/inline_adapter" -require "active_support/core_ext/class/attribute" require "active_support/core_ext/string/inflections" module ActiveJob @@ -9,6 +7,7 @@ module ActiveJob extend ActiveSupport::Concern included do + class_attribute :_queue_adapter_name, instance_accessor: false, instance_predicate: false class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false self.queue_adapter = :async end @@ -21,11 +20,15 @@ module ActiveJob _queue_adapter end + def queue_adapter_name + _queue_adapter_name + end + # 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) - self._queue_adapter = interpret_adapter(name_or_adapter_or_class) + interpret_adapter(name_or_adapter_or_class) end private @@ -33,31 +36,29 @@ module ActiveJob def interpret_adapter(name_or_adapter_or_class) case name_or_adapter_or_class when Symbol, String - ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new + assign_adapter(name_or_adapter_or_class.to_s, + ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new) else if queue_adapter?(name_or_adapter_or_class) - name_or_adapter_or_class - elsif queue_adapter_class?(name_or_adapter_or_class) - ActiveSupport::Deprecation.warn "Passing an adapter class is deprecated " \ - "and will be removed in Rails 5.1. Please pass an adapter name " \ - "(.queue_adapter = :#{name_or_adapter_or_class.name.demodulize.remove('Adapter').underscore}) " \ - "or an instance (.queue_adapter = #{name_or_adapter_or_class.name}.new) instead." - name_or_adapter_or_class.new + 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 end end + def assign_adapter(adapter_name, queue_adapter) + self._queue_adapter_name = adapter_name + self._queue_adapter = queue_adapter + end + QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze def queue_adapter?(object) QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) } end - - def queue_adapter_class?(object) - object.is_a?(Class) && QUEUE_ADAPTER_METHODS.all? { |meth| object.public_method_defined?(meth) } - end end end end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb index 20cc97ebc7..e8994533e4 100644 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -32,7 +32,7 @@ module ActiveJob class JobWrapper < Qu::Job #:nodoc: def initialize(job_data) - @job_data = job_data + @job_data = job_data end def perform diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb index da042cfebf..1b633b210e 100644 --- a/activejob/lib/active_job/queue_adapters/test_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb @@ -38,7 +38,6 @@ module ActiveJob end private - def job_to_hash(job, extras = {}) { job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras) end diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb index 143fac9888..d83113af60 100644 --- a/activejob/lib/active_job/queue_name.rb +++ b/activejob/lib/active_job/queue_name.rb @@ -16,7 +16,7 @@ module ActiveJob # post.to_feed! # end # end - def queue_as(part_name=nil, &block) + def queue_as(part_name = nil, &block) if block_given? self.queue_name = block else @@ -32,11 +32,8 @@ module ActiveJob end included do - class_attribute :queue_name, instance_accessor: false - class_attribute :queue_name_delimiter, instance_accessor: false - - self.queue_name = default_queue_name - self.queue_name_delimiter = "_" # set default delimiter to '_' + class_attribute :queue_name, instance_accessor: false, default: default_queue_name + class_attribute :queue_name_delimiter, instance_accessor: false, default: "_" end # Returns the name of the queue the job will be run on. diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb index a48e53b0ef..db8d9178a4 100644 --- a/activejob/lib/active_job/queue_priority.rb +++ b/activejob/lib/active_job/queue_priority.rb @@ -17,7 +17,7 @@ module ActiveJob # end # # Specify either an argument or a block. - def queue_with_priority(priority=nil, &block) + def queue_with_priority(priority = nil, &block) if block_given? self.priority = block else @@ -27,9 +27,7 @@ module ActiveJob end included do - class_attribute :priority, instance_accessor: false - - self.priority = default_priority + class_attribute :priority, instance_accessor: false, default: default_priority end # Returns the priority that the job will be created with diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb index e4198a40a5..4a8bf04d70 100644 --- a/activejob/lib/active_job/railtie.rb +++ b/activejob/lib/active_job/railtie.rb @@ -15,7 +15,7 @@ module ActiveJob options.queue_adapter ||= :async ActiveSupport.on_load(:active_job) do - options.each { |k,v| send("#{k}=", v) } + options.each { |k, v| send("#{k}=", v) } end end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index bbd2a0c06c..a61e4f59a5 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -8,16 +8,35 @@ module ActiveJob :performed_jobs, :performed_jobs=, to: :queue_adapter + module TestQueueAdapter + extend ActiveSupport::Concern + + included do + class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false + end + + module ClassMethods + def queue_adapter + self._test_adapter.nil? ? super : self._test_adapter + end + + def disable_test_adapter + self._test_adapter = nil + end + + def enable_test_adapter(test_adapter) + self._test_adapter = test_adapter + end + end + end + + ActiveJob::Base.include(TestQueueAdapter) + def before_setup # :nodoc: test_adapter = queue_adapter_for_test - @old_queue_adapters = (ActiveJob::Base.descendants << ActiveJob::Base).select do |klass| - # only override explicitly set adapters, a quirk of `class_attribute` - klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) - end.map do |klass| - [klass, klass.queue_adapter].tap do - klass.queue_adapter = test_adapter - end + queue_adapter_changed_jobs.each do |klass| + klass.enable_test_adapter(test_adapter) end clear_enqueued_jobs @@ -27,9 +46,8 @@ module ActiveJob def after_teardown # :nodoc: super - @old_queue_adapters.each do |(klass, adapter)| - klass.queue_adapter = adapter - end + + queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter } end # Specifies the queue adapter to use with all active job test helpers. @@ -55,7 +73,7 @@ module ActiveJob # assert_enqueued_jobs 2 # end # - # If a block is passed, that block should cause the specified number of + # If a block is passed, that block will cause the specified number of # jobs to be enqueued. # # def test_jobs_again @@ -77,14 +95,23 @@ module ActiveJob # HelloJob.perform_later('jeremy') # end # end - def assert_enqueued_jobs(number, only: nil) + # + # The number of times a job is enqueued to a specific queue can also be asserted. + # + # def test_logging_job + # assert_enqueued_jobs 2, queue: 'default' do + # LoggingJob.perform_later + # HelloJob.perform_later('elfassy') + # end + # end + def assert_enqueued_jobs(number, only: nil, queue: nil) if block_given? - original_count = enqueued_jobs_size(only: only) + original_count = enqueued_jobs_size(only: only, queue: queue) yield - new_count = enqueued_jobs_size(only: only) + new_count = enqueued_jobs_size(only: only, queue: queue) assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" else - actual_count = enqueued_jobs_size(only: only) + actual_count = enqueued_jobs_size(only: only, queue: queue) assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" end end @@ -232,16 +259,16 @@ module ActiveJob # MyJob.set(wait_until: Date.tomorrow.noon).perform_later # end # end - def assert_enqueued_with(args = {}) + def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil) original_enqueued_jobs_count = enqueued_jobs.count - args.assert_valid_keys(:job, :args, :at, :queue) - serialized_args = serialize_args_for_assertion(args) + expected = { job: job, args: args, at: at, queue: queue }.compact + serialized_args = serialize_args_for_assertion(expected) yield in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count) - matching_job = in_block_jobs.find do |job| - serialized_args.all? { |key, value| value == job[key] } + matching_job = in_block_jobs.find do |in_block_job| + serialized_args.all? { |key, value| value == in_block_job[key] } end - assert matching_job, "No enqueued job found with #{args}" + assert matching_job, "No enqueued job found with #{expected}" instantiate_job(matching_job) end @@ -256,19 +283,38 @@ module ActiveJob # MyJob.set(wait_until: Date.tomorrow.noon).perform_later # end # end - def assert_performed_with(args = {}) + def assert_performed_with(job: nil, args: nil, at: nil, queue: nil) original_performed_jobs_count = performed_jobs.count - args.assert_valid_keys(:job, :args, :at, :queue) - serialized_args = serialize_args_for_assertion(args) + expected = { job: job, args: args, at: at, queue: queue }.compact + serialized_args = serialize_args_for_assertion(expected) perform_enqueued_jobs { yield } in_block_jobs = performed_jobs.drop(original_performed_jobs_count) - matching_job = in_block_jobs.find do |job| - serialized_args.all? { |key, value| value == job[key] } + matching_job = in_block_jobs.find do |in_block_job| + serialized_args.all? { |key, value| value == in_block_job[key] } end - assert matching_job, "No performed job found with #{args}" + assert matching_job, "No performed job found with #{expected}" instantiate_job(matching_job) end + # Performs all enqueued jobs in the duration of the block. + # + # def test_perform_enqueued_jobs + # perform_enqueued_jobs do + # MyJob.perform_later(1, 2, 3) + # end + # assert_performed_jobs 1 + # end + # + # This method also supports filtering. If the +:only+ option is specified, + # then only the listed job(s) will be performed. + # + # def test_perform_enqueued_jobs_with_only + # perform_enqueued_jobs(only: MyJob) do + # MyJob.perform_later(1, 2, 3) # will be performed + # HelloJob.perform_later(1, 2, 3) # will not be performed + # end + # assert_performed_jobs 1 + # end def perform_enqueued_jobs(only: nil) old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs @@ -286,39 +332,56 @@ module ActiveJob end end + # Accesses the queue_adapter set by ActiveJob::Base. + # + # def test_assert_job_has_custom_queue_adapter_set + # assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter + # end def queue_adapter ActiveJob::Base.queue_adapter end private - def clear_enqueued_jobs # :nodoc: + def clear_enqueued_jobs enqueued_jobs.clear end - def clear_performed_jobs # :nodoc: + def clear_performed_jobs performed_jobs.clear end - def enqueued_jobs_size(only: nil) # :nodoc: - if only - enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) } - else - enqueued_jobs.count + def enqueued_jobs_size(only: nil, queue: nil) + enqueued_jobs.count do |job| + job_class = job.fetch(:job) + if only + next false unless Array(only).include?(job_class) + end + if queue + next false unless queue.to_s == job.fetch(:queue, job_class.queue_name) + end + true end end - def serialize_args_for_assertion(args) # :nodoc: + def serialize_args_for_assertion(args) args.dup.tap do |serialized_args| serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args] serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at] end end - def instantiate_job(payload) # :nodoc: + def instantiate_job(payload) job = payload[:job].new(*payload[:args]) job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at) job.queue_name = payload[:queue] job end + + def queue_adapter_changed_jobs + (ActiveJob::Base.descendants << ActiveJob::Base).select do |klass| + # only override explicitly set adapters, a quirk of `class_attribute` + klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) + end + end end end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 97c11a9ea6..474f181f65 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -12,14 +12,14 @@ module Rails # :nodoc: hook_for :test_framework def self.default_generator_root - File.dirname(__FILE__) + __dir__ end def create_job_file template "job.rb", File.join("app/jobs", class_path, "#{file_name}_job.rb") in_root do - if self.behavior == :invoke && !File.exist?(application_job_file_name) + if behavior == :invoke && !File.exist?(application_job_file_name) template "application_job.rb", application_job_file_name end end diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb index 5f0ee2418c..98e41c0c36 100644 --- a/activejob/test/adapters/delayed_job.rb +++ b/activejob/test/adapters/delayed_job.rb @@ -1,6 +1,6 @@ ActiveJob::Base.queue_adapter = :delayed_job -$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job" +$LOAD_PATH << File.expand_path("../support/delayed_job", __dir__) Delayed::Worker.delay_jobs = false Delayed::Worker.backend = :test diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 954974b2a5..d5ca0f385c 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -5,6 +5,7 @@ require "jobs/hello_job" require "jobs/logging_job" require "jobs/overridden_logging_job" require "jobs/nested_job" +require "jobs/rescue_job" require "models/person" class LoggingTest < ActiveSupport::TestCase @@ -89,21 +90,21 @@ class LoggingTest < ActiveSupport::TestCase def test_perform_job_logging LoggingJob.perform_later "Dummy" - assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages) + assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages) assert_match(/Dummy, here is it: Dummy/, @logger.messages) - assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages) + assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages) end def test_perform_nested_jobs_logging NestedJob.perform_later assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages) assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages) assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages) end def test_enqueue_at_job_logging @@ -124,4 +125,11 @@ class LoggingTest < ActiveSupport::TestCase set_logger ::Logger.new(nil) OverriddenLoggingJob.perform_later "Dummy" end + + def test_job_error_logging + RescueJob.perform_later "other" + rescue RescueJob::OtherError + assert_match(/Performing RescueJob \(Job ID: .*?\) from .*? with arguments:.*other/, @logger.messages) + assert_match(/Error performing RescueJob \(Job ID: .*?\) from .*? in .*ms: RescueJob::OtherError \(Bad hair\):\n.*\brescue_job\.rb:\d+:in `perform'/, @logger.messages) + end end diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb index dc862450aa..8368107bdf 100644 --- a/activejob/test/cases/queue_adapter_test.rb +++ b/activejob/test/cases/queue_adapter_test.rb @@ -20,31 +20,24 @@ class QueueAdapterTest < ActiveJob::TestCase assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex.new } end - test "should warn on passing an adapter class" do - klass = Class.new do - def self.name - "fake" - end - - def enqueue(*); end - def enqueue_at(*); end - end - - assert_deprecated { ActiveJob::Base.queue_adapter = klass } - end - test "should allow overriding the queue_adapter at the child class level without affecting the parent or its sibling" do + ActiveJob::Base.disable_test_adapter base_queue_adapter = ActiveJob::Base.queue_adapter child_job_one = Class.new(ActiveJob::Base) + assert_equal child_job_one.queue_adapter_name, ActiveJob::Base.queue_adapter_name + child_job_one.queue_adapter = :stub_one assert_not_equal ActiveJob::Base.queue_adapter, child_job_one.queue_adapter + assert_equal "stub_one", child_job_one.queue_adapter_name assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter child_job_two = Class.new(ActiveJob::Base) child_job_two.queue_adapter = :stub_two + assert_equal "stub_two", child_job_two.queue_adapter_name + assert_kind_of ActiveJob::QueueAdapters::StubTwoAdapter, child_job_two.queue_adapter assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter, "child_job_one's queue adapter should remain unchanged" assert_equal base_queue_adapter, ActiveJob::Base.queue_adapter, "ActiveJob::Base's queue adapter should remain unchanged" diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb index 7777e557c9..0247bc111e 100644 --- a/activejob/test/cases/queue_naming_test.rb +++ b/activejob/test/cases/queue_naming_test.rb @@ -56,7 +56,7 @@ class QueueNamingTest < ActiveSupport::TestCase original_queue_name = HelloJob.queue_name begin - HelloJob.queue_as { self.arguments.first=="1" ? :one : :two } + HelloJob.queue_as { arguments.first == "1" ? :one : :two } assert_equal "one", HelloJob.new("1").queue_name assert_equal "two", HelloJob.new("3").queue_name ensure diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb index ab4a1bdf7b..171fb1e593 100644 --- a/activejob/test/cases/queue_priority_test.rb +++ b/activejob/test/cases/queue_priority_test.rb @@ -3,7 +3,7 @@ require "jobs/hello_job" class QueuePriorityTest < ActiveSupport::TestCase test "priority unset by default" do - assert_equal nil, HelloJob.priority + assert_nil HelloJob.priority end test "uses given priority" do @@ -32,7 +32,7 @@ class QueuePriorityTest < ActiveSupport::TestCase original_priority = HelloJob.priority begin - HelloJob.queue_with_priority { self.arguments.first=="1" ? 99 : 11 } + HelloJob.queue_with_priority { arguments.first == "1" ? 99 : 11 } assert_equal 99, HelloJob.new("1").priority assert_equal 11, HelloJob.new("3").priority ensure diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 253c557cc5..81e75b4374 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -110,6 +110,27 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_jobs_with_only_and_queue_option + assert_nothing_raised do + assert_enqueued_jobs 1, only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.perform_later + end + end + end + + def test_assert_enqueued_jobs_with_queue_option + assert_nothing_raised do + assert_enqueued_jobs 2, queue: :default do + HelloJob.perform_later + LoggingJob.perform_later + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.set(queue: :other_queue).perform_later + end + end + end + def test_assert_enqueued_jobs_with_only_option_and_none_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_jobs 1, only: HelloJob do @@ -250,17 +271,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase HelloJob.perform_later end - assert_equal 2, ActiveJob::Base.queue_adapter.enqueued_jobs.count + assert_equal 2, queue_adapter.enqueued_jobs.count end end class PerformedJobsTest < ActiveJob::TestCase def test_performed_enqueue_jobs_with_only_option_doesnt_leak_outside_the_block - assert_equal nil, queue_adapter.filter + assert_nil queue_adapter.filter perform_enqueued_jobs only: HelloJob do assert_equal HelloJob, queue_adapter.filter end - assert_equal nil, queue_adapter.filter + assert_nil queue_adapter.filter end def test_assert_performed_jobs @@ -507,7 +528,7 @@ class PerformedJobsTest < ActiveJob::TestCase HelloJob.perform_later end - assert_equal 2, ActiveJob::Base.queue_adapter.performed_jobs.count + assert_equal 2, queue_adapter.performed_jobs.count end end @@ -528,3 +549,20 @@ class InheritedJobTest < ActiveJob::TestCase assert_instance_of ActiveJob::QueueAdapters::TestAdapter, InheritedJob.queue_adapter end end + +class QueueAdapterJobTest < ActiveJob::TestCase + def before_setup + @original_autoload_paths = ActiveSupport::Dependencies.autoload_paths + ActiveSupport::Dependencies.autoload_paths = %w(test/jobs) + super + end + + def after_teardown + ActiveSupport::Dependencies.autoload_paths = @original_autoload_paths + super + end + + def test_queue_adapter_is_test_adapter + assert_instance_of ActiveJob::QueueAdapters::TestAdapter, QueueAdapterJob.queue_adapter + end +end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index dbc7dad109..db07ecab7a 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -1,10 +1,10 @@ require "active_job" require "support/job_buffer" -ActiveSupport.halt_callback_chains_on_return_false = false GlobalID.app = "aj" -@adapter = ENV["AJ_ADAPTER"] || "inline" +@adapter = ENV["AJ_ADAPTER"] || "inline" +puts "Using #{@adapter}" if ENV["AJ_INTEGRATION_TESTS"] require "support/integration/helper" diff --git a/activejob/test/jobs/application_job.rb b/activejob/test/jobs/application_job.rb index 4a78b890ec..a009ace51c 100644 --- a/activejob/test/jobs/application_job.rb +++ b/activejob/test/jobs/application_job.rb @@ -1,4 +1,2 @@ -require_relative "../support/job_buffer" - class ApplicationJob < ActiveJob::Base end diff --git a/activejob/test/jobs/queue_adapter_job.rb b/activejob/test/jobs/queue_adapter_job.rb new file mode 100644 index 0000000000..160dfd74ec --- /dev/null +++ b/activejob/test/jobs/queue_adapter_job.rb @@ -0,0 +1,3 @@ +class QueueAdapterJob < ActiveJob::Base + self.queue_adapter = :inline +end diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb index ef8f777437..62add4271a 100644 --- a/activejob/test/jobs/rescue_job.rb +++ b/activejob/test/jobs/rescue_job.rb @@ -19,7 +19,7 @@ class RescueJob < ActiveJob::Base when "david" raise ArgumentError, "Hair too good" when "other" - raise OtherError + raise OtherError, "Bad hair" else JobBuffer.add("performed beautifully") end diff --git a/activejob/test/models/person.rb b/activejob/test/models/person.rb index 76a8f40616..b5d68ad9c1 100644 --- a/activejob/test/models/person.rb +++ b/activejob/test/models/person.rb @@ -6,7 +6,7 @@ class Person attr_reader :id def self.find(id) - raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i==404 + raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i == 404 new(id) end diff --git a/activejob/test/support/backburner/inline.rb b/activejob/test/support/backburner/inline.rb index 647dbf317f..9758332b6f 100644 --- a/activejob/test/support/backburner/inline.rb +++ b/activejob/test/support/backburner/inline.rb @@ -2,7 +2,7 @@ require "backburner" Backburner::Worker.class_eval do class << self; alias_method :original_enqueue, :enqueue; end - def self.enqueue(job_class, args=[], opts={}) + def self.enqueue(job_class, args = [], opts = {}) job_class.perform(*args) end end diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb index a900b18e2a..98d731ff1e 100644 --- a/activejob/test/support/delayed_job/delayed/backend/test.rb +++ b/activejob/test/support/delayed_job/delayed/backend/test.rb @@ -26,7 +26,7 @@ module Delayed self.attempts = 0 self.priority = 0 self.id = (self.class.id += 1) - hash.each { |k,v| send(:"#{k}=", v) } + hash.each { |k, v| send(:"#{k}=", v) } end @jobs = [] @@ -63,7 +63,7 @@ module Delayed jobs = jobs.select { |j| Worker.queues.include?(j.queue) } if Worker.queues.any? jobs = jobs.select { |j| j.priority >= Worker.min_priority } if Worker.min_priority jobs = jobs.select { |j| j.priority <= Worker.max_priority } if Worker.max_priority - jobs.sort_by { |j| [j.priority, j.run_at] }[0..limit-1] + jobs.sort_by { |j| [j.priority, j.run_at] }[0..limit - 1] end # Lock this job for this worker. @@ -84,7 +84,7 @@ module Delayed end def update_attributes(attrs = {}) - attrs.each { |k,v| send(:"#{k}=", v) } + attrs.each { |k, v| send(:"#{k}=", v) } save end diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb index 263097c792..2e194933a1 100644 --- a/activejob/test/support/integration/adapters/backburner.rb +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -23,12 +23,12 @@ module BackburnerJobsManager end def tube - @tube ||= Beaneater::Tube.new(Backburner::Worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name + @tube ||= Beaneater::Tube.new(@worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name end def can_run? begin - Backburner::Worker.connection.send :connect! + @worker = Backburner::Worker.new rescue return false end diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index 3d35a0439f..20faee3427 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -13,7 +13,7 @@ module QueJobsManager def start_workers que_url = ENV["QUE_DATABASE_URL"] || "postgres:///active_jobs_que_int_test" uri = URI.parse(que_url) - user = uri.user||ENV["USER"] + user = uri.user || ENV["USER"] pass = uri.password db = uri.path[1..-1] %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index b5d831428e..369693e947 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -12,7 +12,7 @@ module QueueClassicJobsManager def start_workers uri = URI.parse(ENV["QC_DATABASE_URL"]) - user = uri.user||ENV["USER"] + user = uri.user || ENV["USER"] pass = uri.password db = uri.path[1..-1] %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb index 08743c1f05..1c8dfaca59 100644 --- a/activejob/test/support/integration/adapters/sneakers.rb +++ b/activejob/test/support/integration/adapters/sneakers.rb @@ -39,7 +39,7 @@ module SneakersJobsManager @pid = fork do queues = %w(integration_tests) workers = queues.map do |q| - worker_klass = "ActiveJobWorker"+Digest::MD5.hexdigest(q) + worker_klass = "ActiveJobWorker" + Digest::MD5.hexdigest(q) Sneakers.const_set(worker_klass, Class.new(ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper) do from_queue q end) @@ -73,7 +73,7 @@ module SneakersJobsManager true end - protected + private def bunny_publisher @bunny_publisher ||= begin p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher) diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 62f6fa13f6..14fe3c9adc 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -5,7 +5,7 @@ end rails_command("db:migrate") initializer "activejob.rb", <<-CODE -require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" +require "#{File.expand_path("jobs_manager.rb", __dir__)}" JobsManager.current_manager.setup CODE @@ -18,12 +18,13 @@ class TestJob < ActiveJob::Base queue_as :integration_tests def perform(x) - File.open(Rails.root.join("tmp/\#{x}"), "wb+") do |f| + File.open(Rails.root.join("tmp/\#{x}.new"), "wb+") do |f| f.write Marshal.dump({ "locale" => I18n.locale.to_s || "en", "executed_at" => Time.now.to_r }) end + File.rename(Rails.root.join("tmp/\#{x}.new"), Rails.root.join("tmp/\#{x}")) end end CODE diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb index 1aaee2c809..545b62752e 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -5,8 +5,9 @@ ActiveJob::Base.queue_name_prefix = nil require "rails/generators/rails/app/app_generator" +require "tmpdir" dummy_app_path = Dir.mktmpdir + "/dummy" -dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__) +dummy_app_template = File.expand_path("dummy_app_template.rb", __dir__) args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle", "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quiet", "--template", dummy_app_template]).prepare! diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index 4c4c56c9da..41bf9c89d1 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -1,4 +1,3 @@ -require "active_support/concern" require "active_support/core_ext/string/inflections" require "support/integration/jobs_manager" @@ -18,7 +17,7 @@ module TestCaseHelpers end end - protected + private def jobs_manager JobsManager.current_manager @@ -33,7 +32,7 @@ module TestCaseHelpers adapter_class_symbols.map(&:to_s).include? adapter end - def wait_for_jobs_to_finish_for(seconds=60) + def wait_for_jobs_to_finish_for(seconds = 60) begin Timeout.timeout(seconds) do while !job_executed do @@ -48,7 +47,7 @@ module TestCaseHelpers Dummy::Application.root.join("tmp/#{id}") end - def job_executed(id=@id) + def job_executed(id = @id) job_file(id).exist? end @@ -56,11 +55,11 @@ module TestCaseHelpers Marshal.load(File.binread(job_file(id))) end - def job_executed_at(id=@id) + def job_executed_at(id = @id) job_data(id)["executed_at"] end - def job_executed_in_locale(id=@id) + def job_executed_in_locale(id = @id) job_data(id)["locale"] end end diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb index 3cdc54e6d5..cf102ae5c2 100644 --- a/activejob/test/support/sneakers/inline.rb +++ b/activejob/test/support/sneakers/inline.rb @@ -4,7 +4,7 @@ module Sneakers module Worker module ClassMethods def enqueue(msg) - worker = self.new(nil, nil, {}) + worker = new(nil, nil, {}) worker.work(*msg) end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 206699c036..7483704212 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,2 +1,37 @@ +* Fix regression in numericality validator when comparing Decimal and Float input + values with more scale than the schema. -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md) for previous changes. + *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. diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index 8573eb1225..ac810e86d0 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/Rakefile b/activemodel/Rakefile index c7f97a4258..d60f6d9997 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,14 +1,12 @@ require "rake/testtask" -dir = File.dirname(__FILE__) - task default: :test task :package Rake::TestTask.new do |t| t.libs << "test" - t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb") + t.test_files = Dir.glob("#{__dir__}/test/cases/**/*_test.rb") t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -16,8 +14,8 @@ end namespace :test do task :isolated do - Dir.glob("#{dir}/test/**/*_test.rb").all? do |file| - sh(Gem.ruby, "-w", "-I#{dir}/lib", "-I#{dir}/test", file) + Dir.glob("#{__dir__}/test/**/*_test.rb").all? do |file| + sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file) end || raise("Failures") end end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index fd715f6ba9..43f1e09c77 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/activemodel/bin/test b/activemodel/bin/test index 84a05bba08..a7beb14b27 100755 --- a/activemodel/bin/test +++ b/activemodel/bin/test @@ -2,5 +2,3 @@ COMPONENT_ROOT = File.expand_path("..", __dir__) require File.expand_path("../tools/test", COMPONENT_ROOT) - -exit Minitest.run(ARGV) diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index a9b0663940..a2892e9ea9 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 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,7 +42,6 @@ module ActiveModel autoload :Naming autoload :SecurePassword autoload :Serialization - autoload :TestCase autoload :Translation autoload :Validations autoload :Validator @@ -69,5 +68,5 @@ module ActiveModel end ActiveSupport.on_load(:i18n) do - I18n.load_path << File.dirname(__FILE__) + "/active_model/locale/en.yml" + I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__) end diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 7dad3b6dff..930e89d611 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -19,15 +19,15 @@ module ActiveModel # cat = Cat.new # cat.assign_attributes(name: "Gorby", status: "yawning") # cat.name # => 'Gorby' - # cat.status => 'yawning' + # cat.status # => 'yawning' # cat.assign_attributes(status: "sleeping") # cat.name # => 'Gorby' - # cat.status => 'sleeping' + # cat.status # => 'sleeping' def assign_attributes(new_attributes) if !new_attributes.respond_to?(:stringify_keys) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." end - return if new_attributes.nil? || new_attributes.empty? + return if new_attributes.empty? attributes = new_attributes.stringify_keys _assign_attributes(sanitize_for_mass_assignment(attributes)) diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 96709486f3..b3b39bf7ae 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,6 +1,5 @@ require "concurrent/map" require "mutex_m" -require "active_support/core_ext/regexp" module ActiveModel # Raised when an attribute is not defined. @@ -69,9 +68,8 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false - self.attribute_aliases = {} - self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] + class_attribute :attribute_aliases, instance_writer: false, default: {} + class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ] end module ClassMethods @@ -290,7 +288,7 @@ module ActiveModel generate_method = "define_method_#{matcher.method_missing_target}" if respond_to?(generate_method, true) - send(generate_method, attr_name) + send(generate_method, attr_name.to_s) else define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end @@ -335,12 +333,11 @@ module ActiveModel }.tap { |mod| include mod } end - protected - def instance_method_already_implemented?(method_name) #:nodoc: + private + def instance_method_already_implemented?(method_name) generated_attribute_methods.method_defined?(method_name) end - private # The methods +method_missing+ and +respond_to?+ of this module are # invoked often in a typical rails, both of which invoke the method # +matched_attribute_method+. The latter method iterates through an @@ -350,11 +347,11 @@ module ActiveModel # used to alleviate the GC, which ultimately also speeds up the app # significantly (in our case our test suite finishes 10% faster with # this cache). - def attribute_method_matchers_cache #:nodoc: + def attribute_method_matchers_cache @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4) end - def attribute_method_matchers_matching(method_name) #:nodoc: + def attribute_method_matchers_matching(method_name) attribute_method_matchers_cache.compute_if_absent(method_name) do # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix # will match every time. @@ -366,7 +363,7 @@ module ActiveModel # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. - def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: + def define_proxy_call(include_private, mod, name, send, *extra) defn = if NAME_COMPILABLE_REGEXP.match?(name) "def #{name}(*args)" else @@ -459,12 +456,11 @@ module ActiveModel end end - protected - def attribute_method?(attr_name) #:nodoc: + private + def attribute_method?(attr_name) respond_to_without_attributes?(:attributes) && attributes.include?(attr_name) end - private # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. def matched_attribute_method(method_name) @@ -475,5 +471,9 @@ module ActiveModel def missing_attribute(attr_name, stack) raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack end + + def _read_attribute(attr) + __send__(attr) + end end end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 283090e380..835e6f7716 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -56,6 +56,9 @@ module ActiveModel # # Would only create the +after_create+ and +before_create+ callback methods in # your class. + # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks def self.extended(base) #:nodoc: base.class_eval do @@ -103,7 +106,6 @@ module ActiveModel def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { - terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] @@ -122,19 +124,19 @@ module ActiveModel private - def _define_before_model_callback(klass, callback) #:nodoc: + def _define_before_model_callback(klass, callback) klass.define_singleton_method("before_#{callback}") do |*args, &block| set_callback(:"#{callback}", :before, *args, &block) end end - def _define_around_model_callback(klass, callback) #:nodoc: + def _define_around_model_callback(klass, callback) klass.define_singleton_method("around_#{callback}") do |*args, &block| set_callback(:"#{callback}", :around, *args, &block) end end - def _define_after_model_callback(klass, callback) #:nodoc: + def _define_after_model_callback(klass, callback) klass.define_singleton_method("after_#{callback}") do |*args, &block| options = args.extract_options! options[:prepend] = true diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 6e0af99ad7..dc81f74779 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -179,13 +179,13 @@ module ActiveModel # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: !!changes_include?(attr) && - (to == OPTION_NOT_GIVEN || to == __send__(attr)) && + (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) end # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. @@ -226,7 +226,7 @@ module ActiveModel # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) - [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) + [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. @@ -239,7 +239,7 @@ module ActiveModel return if attribute_changed?(attr) begin - value = __send__(attr) + value = _read_attribute(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 191039f598..942b4fa9bb 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -110,49 +110,21 @@ module ActiveModel # person.errors.include?(:name) # => true # person.errors.include?(:age) # => false def include?(attribute) + attribute = attribute.to_sym messages.key?(attribute) && messages[attribute].present? end alias :has_key? :include? alias :key? :include? - # Get messages for +key+. - # - # person.errors.messages # => {:name=>["cannot be nil"]} - # person.errors.get(:name) # => ["cannot be nil"] - # person.errors.get(:age) # => [] - def get(key) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1. - - To achieve the same use model.errors[:#{key}]. - MESSAGE - - messages[key] - end - - # Set messages for +key+ to +value+. - # - # person.errors[:name] # => ["cannot be nil"] - # person.errors.set(:name, ["can't be nil"]) - # person.errors[:name] # => ["can't be nil"] - def set(key, value) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. - - Use model.errors.add(:#{key}, #{value.inspect}) instead. - MESSAGE - - messages[key] = value - end - # Delete messages for +key+. Returns the deleted messages. # # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] # person.errors[:name] # => [] def delete(key) - details.delete(key) - messages.delete(key) + attribute = key.to_sym + details.delete(attribute) + messages.delete(attribute) end # When passed a symbol or a name of a method, returns an array of errors @@ -160,33 +132,10 @@ module ActiveModel # # person.errors[:name] # => ["cannot be nil"] # person.errors['name'] # => ["cannot be nil"] - # - # Note that, if you try to get errors of an attribute which has - # no errors associated with it, this method will instantiate - # an empty error list for it and +keys+ will return an array - # of error keys which includes this attribute. - # - # person.errors.keys # => [] - # person.errors[:name] # => [] - # person.errors.keys # => [:name] def [](attribute) messages[attribute.to_sym] end - # Adds to the supplied attribute the supplied error message. - # - # person.errors[:name] = "must be set" - # person.errors[:name] # => ['must be set'] - def []=(attribute, error) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1. - - Use model.errors.add(:#{attribute}, #{error.inspect}) instead. - MESSAGE - - messages[attribute.to_sym] << error - end - # Iterates through each error key, value pair in the error messages hash. # Yields the attribute and the error for that attribute. If the attribute # has more than one error message, yields once for each error message. @@ -223,7 +172,9 @@ module ActiveModel # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.values # => [["cannot be nil", "must be specified"]] def values - messages.values + messages.select do |key, value| + !value.empty? + end.values end # Returns all message keys. @@ -231,7 +182,9 @@ module ActiveModel # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.keys # => [:name] def keys - messages.keys + messages.select do |key, value| + !value.empty? + end.keys end # Returns +true+ if no errors are found, +false+ otherwise. @@ -255,7 +208,7 @@ module ActiveModel # # <error>name can't be blank</error> # # <error>name must be specified</error> # # </errors> - def to_xml(options={}) + def to_xml(options = {}) to_a.to_xml({ root: "errors", skip_types: true }.merge!(options)) end @@ -265,7 +218,7 @@ module ActiveModel # # person.errors.as_json # => {:name=>["cannot be nil"]} # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]} - def as_json(options=nil) + def as_json(options = nil) to_hash(options && options[:full_messages]) end @@ -280,7 +233,7 @@ module ActiveModel messages[attribute] = array.map { |message| full_message(attribute, message) } end else - messages.dup + without_default_proc(messages) end end @@ -338,49 +291,6 @@ module ActiveModel messages[attribute.to_sym] << message end - # Will add an error message to each of the attributes in +attributes+ - # that is empty. - # - # person.errors.add_on_empty(:name) - # person.errors.messages - # # => {:name=>["can't be empty"]} - def add_on_empty(attributes, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1. - - To achieve the same use: - - errors.add(attribute, :empty, options) if value.nil? || value.empty? - MESSAGE - - Array(attributes).each do |attribute| - value = @base.send(:read_attribute_for_validation, attribute) - is_empty = value.respond_to?(:empty?) ? value.empty? : false - add(attribute, :empty, options) if value.nil? || is_empty - end - end - - # Will add an error message to each of the attributes in +attributes+ that - # is blank (using Object#blank?). - # - # person.errors.add_on_blank(:name) - # person.errors.messages - # # => {:name=>["can't be blank"]} - def add_on_blank(attributes, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1. - - To achieve the same use: - - errors.add(attribute, :blank, options) if value.blank? - MESSAGE - - Array(attributes).each do |attribute| - value = @base.send(:read_attribute_for_validation, attribute) - add(attribute, :blank, options) if value.blank? - end - end - # Returns +true+ if an error on the attribute with the given message is # present, or +false+ otherwise. +message+ is treated the same as for +add+. # @@ -429,6 +339,7 @@ module ActiveModel # person.errors.full_messages_for(:name) # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"] def full_messages_for(attribute) + attribute = attribute.to_sym messages[attribute].map { |message| full_message(attribute, message) } end @@ -503,16 +414,23 @@ module ActiveModel I18n.translate(key, options) end - def marshal_dump + def marshal_dump # :nodoc: [@base, without_default_proc(@messages), without_default_proc(@details)] end - def marshal_load(array) + def marshal_load(array) # :nodoc: @base, @messages, @details = array apply_default_array(@messages) apply_default_array(@details) end + def init_with(coder) # :nodoc: + coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) } + @details ||= {} + apply_default_array(@messages) + apply_default_array(@details) + end + private def normalize_message(attribute, message, options) case message diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index d2c6a89cc2..45ab8a2ca1 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -15,7 +15,7 @@ module ActiveModel end module ForbiddenAttributesProtection # :nodoc: - protected + private def sanitize_for_mass_assignment(attributes) if attributes.respond_to?(:permitted?) raise ActiveModel::ForbiddenAttributesError if !attributes.permitted? diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 4a8ee915cf..67bdfaa643 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -6,7 +6,7 @@ module ActiveModel module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index f5765c0c54..945a5402a3 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -2,10 +2,10 @@ module ActiveModel # == Active \Model \Basic \Model # # Includes the required interface for an object to interact with - # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules. + # Action Pack and Action View, using different Active Model modules. # It includes model name introspections, conversions, translations and # validations. Besides that, it allows you to initialize the object with a - # hash of attributes, pretty much like <tt>ActiveRecord</tt> does. + # hash of attributes, pretty much like Active Record does. # # A minimal implementation could be: # @@ -75,7 +75,7 @@ module ActiveModel # person = Person.new(name: 'bob', age: '18') # person.name # => "bob" # person.age # => "18" - def initialize(attributes={}) + def initialize(attributes = {}) assign_attributes(attributes) if attributes super() diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 3830d1486d..9853cf38fe 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,7 +1,6 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/module/introspection" require "active_support/core_ext/module/remove_method" -require "active_support/core_ext/module/delegation" module ActiveModel class Name @@ -174,7 +173,7 @@ module ActiveModel # BlogPost.model_name.human # => "Blog post" # # Specify +options+ with additional translating options. - def human(options={}) + def human(options = {}) return @human unless @klass.respond_to?(:lookup_ancestors) && @klass.respond_to?(:i18n_scope) @@ -235,7 +234,7 @@ module ActiveModel # Person.model_name.plural # => "people" def model_name @_model_name ||= begin - namespace = self.parents.detect do |n| + namespace = parents.detect do |n| n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming? end ActiveModel::Name.new(self, namespace) diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index b5e030a59b..205b84ddb4 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,8 +10,7 @@ module ActiveModel included do extend ActiveModel::Naming - class_attribute :include_root_in_json, instance_writer: false - self.include_root_in_json = false + class_attribute :include_root_in_json, instance_writer: false, default: false end # Returns a hash representing the model. Some configuration can be @@ -134,7 +133,7 @@ module ActiveModel # person.name # => "bob" # person.age # => 22 # person.awesome # => true - def from_json(json, include_root=include_root_in_json) + def from_json(json, include_root = include_root_in_json) hash = ActiveSupport::JSON.decode(json) hash = hash.values.first if include_root self.attributes = hash diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb deleted file mode 100644 index 5004855d56..0000000000 --- a/activemodel/lib/active_model/test_case.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ActiveModel #:nodoc: - class TestCase < ActiveSupport::TestCase #:nodoc: - end -end diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index b8cf43cc10..35fc7cf743 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -44,7 +44,7 @@ module ActiveModel parts = attribute.to_s.split(".") attribute = parts.pop namespace = parts.join("/") unless parts.empty? - attributes_scope = "#{self.i18n_scope}.attributes" + attributes_scope = "#{i18n_scope}.attributes" if namespace defaults = lookup_ancestors.map do |klass| diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index 313f17830f..095801d8f0 100644 --- a/activemodel/lib/active_model/type.rb +++ b/activemodel/lib/active_model/type.rb @@ -7,14 +7,11 @@ require "active_model/type/boolean" require "active_model/type/date" require "active_model/type/date_time" require "active_model/type/decimal" -require "active_model/type/decimal_without_scale" require "active_model/type/float" require "active_model/type/immutable_string" require "active_model/type/integer" require "active_model/type/string" -require "active_model/type/text" require "active_model/type/time" -require "active_model/type/unsigned_integer" require "active_model/type/registry" @@ -24,16 +21,8 @@ module ActiveModel class << self attr_accessor :registry # :nodoc: - delegate :add_modifier, to: :registry - # Add a new type to the registry, allowing it to be referenced as a - # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your - # type is only meant to be used with a specific database adapter, you can - # do so by passing +adapter: :postgresql+. If your type has the same - # name as a native type for the current adapter, an exception will be - # raised unless you specify an +:override+ option. +override: true+ will - # cause your type to be used instead of the native type. +override: - # false+ will cause the native type to be used over yours if one exists. + # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup def register(type_name, klass = nil, **options, &block) registry.register(type_name, klass, **options, &block) end @@ -53,7 +42,6 @@ module ActiveModel register(:immutable_string, Type::ImmutableString) register(:integer, Type::Integer) register(:string, Type::String) - register(:text, Type::Text) register(:time, Type::Time) end end diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb index 6e313fbca8..eefd080351 100644 --- a/activemodel/lib/active_model/type/date.rb +++ b/activemodel/lib/active_model/type/date.rb @@ -12,7 +12,7 @@ module ActiveModel end def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" + value.to_s(:db).inspect end private diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index 6c5c0451c6..e6805c5f6b 100644 --- a/activemodel/lib/active_model/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -4,6 +4,7 @@ module ActiveModel module Type class Decimal < Value # :nodoc: include Helpers::Numeric + BIGDECIMAL_PRECISION = 18 def type :decimal @@ -20,8 +21,14 @@ module ActiveModel case value when ::Float convert_float_to_big_decimal(value) - when ::Numeric, ::String - BigDecimal(value, precision.to_i) + when ::Numeric + BigDecimal(value, precision || BIGDECIMAL_PRECISION) + when ::String + begin + value.to_d + rescue ArgumentError + BigDecimal(0) + end else if value.respond_to?(:to_d) value.to_d diff --git a/activemodel/lib/active_model/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb deleted file mode 100644 index 985e1038ed..0000000000 --- a/activemodel/lib/active_model/type/decimal_without_scale.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "active_model/type/big_integer" - -module ActiveModel - module Type - class DecimalWithoutScale < BigInteger # :nodoc: - def type - :decimal - end - end - end -end diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb index 94bb7e700c..4d0d2771a0 100644 --- a/activemodel/lib/active_model/type/float.rb +++ b/activemodel/lib/active_model/type/float.rb @@ -7,6 +7,15 @@ module ActiveModel :float end + def type_cast_for_schema(value) + return "::Float::NAN" if value.try(:nan?) + case value + when ::Float::INFINITY then "::Float::INFINITY" + when -::Float::INFINITY then "-::Float::INFINITY" + else super + end + end + alias serialize cast private diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb index facea12704..f783d286c5 100644 --- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -1,7 +1,7 @@ module ActiveModel module Type - module Helpers - class AcceptsMultiparameterTime < Module # :nodoc: + module Helpers # :nodoc: all + class AcceptsMultiparameterTime < Module def initialize(defaults: {}) define_method(:cast) do |value| if value.is_a?(Hash) diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb index 4dddbe4e5e..f3a17a1698 100644 --- a/activemodel/lib/active_model/type/helpers/mutable.rb +++ b/activemodel/lib/active_model/type/helpers/mutable.rb @@ -1,7 +1,7 @@ module ActiveModel module Type - module Helpers - module Mutable # :nodoc: + module Helpers # :nodoc: all + module Mutable def cast(value) deserialize(serialize(value)) end diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index 98533f8771..275822b738 100644 --- a/activemodel/lib/active_model/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -1,7 +1,7 @@ module ActiveModel module Type - module Helpers - module Numeric # :nodoc: + module Helpers # :nodoc: all + module Numeric def cast(value) value = \ case value diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index d8c5d929a3..53cf7c6029 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -2,8 +2,8 @@ require "active_support/core_ext/time/zones" module ActiveModel module Type - module Helpers - module TimeValue # :nodoc: + module Helpers # :nodoc: all + module TimeValue def serialize(value) value = apply_seconds_precision(value) @@ -33,12 +33,12 @@ module ActiveModel def apply_seconds_precision(value) return value unless precision && value.respond_to?(:usec) number_of_insignificant_digits = 6 - precision - round_power = 10 ** number_of_insignificant_digits - value.change(usec: value.usec / round_power * round_power) + round_power = 10**number_of_insignificant_digits + value.change(usec: value.usec - value.usec % round_power) end def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" + value.to_s(:db).inspect end def user_input_in_time_zone(value) diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index 41dd655a5c..106b5d966c 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -29,6 +29,8 @@ module ActiveModel result end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :range @@ -46,7 +48,7 @@ module ActiveModel def ensure_in_range(value) unless range.cover?(value) - raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" + raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes" end end diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb index d25f1a129e..2d5dd366eb 100644 --- a/activemodel/lib/active_model/type/registry.rb +++ b/activemodel/lib/active_model/type/registry.rb @@ -21,6 +21,8 @@ module ActiveModel end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :registrations @@ -55,6 +57,8 @@ module ActiveModel type_name == name end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :block diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb index c7e0208a5a..850cab962b 100644 --- a/activemodel/lib/active_model/type/string.rb +++ b/activemodel/lib/active_model/type/string.rb @@ -12,7 +12,12 @@ module ActiveModel private def cast_value(value) - ::String.new(super) + case value + when ::String then ::String.new(value) + when true then "t".freeze + when false then "f".freeze + else value.to_s + end end end end diff --git a/activemodel/lib/active_model/type/text.rb b/activemodel/lib/active_model/type/text.rb deleted file mode 100644 index 7c0d647706..0000000000 --- a/activemodel/lib/active_model/type/text.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "active_model/type/string" - -module ActiveModel - module Type - class Text < String # :nodoc: - def type - :text - end - end - end -end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index df6d3f2bcb..ae1d69f685 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -49,8 +49,7 @@ module ActiveModel private :validation_context= define_callbacks :validate, scope: :name - class_attribute :_validators, instance_writer: false - self._validators = Hash.new { |h,k| h[k] = [] } + class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] } end module ClassMethods @@ -68,7 +67,7 @@ module ActiveModel # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) @@ -134,7 +133,7 @@ module ActiveModel # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) @@ -147,6 +146,9 @@ module ActiveModel # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. + # + # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions. + # def validate(*args, &block) options = args.extract_options! @@ -399,14 +401,14 @@ module ActiveModel # end alias :read_attribute_for_validation :send - protected + private - def run_validations! #:nodoc: + def run_validations! _run_validate_callbacks errors.empty? end - def raise_validation_error + def raise_validation_error # :doc: raise(ValidationError.new(self)) end end @@ -432,4 +434,4 @@ module ActiveModel end end -Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file } +Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file } diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 75bf655578..4618f46e30 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -22,7 +22,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_absence_of(*attr_names) validates_with AbsenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 4fadc795cd..a26c37daa5 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -24,7 +24,7 @@ module ActiveModel class LazilyDefineAttributes < Module def initialize(attribute_definition) - define_method(:respond_to_missing?) do |method_name, include_private=false| + define_method(:respond_to_missing?) do |method_name, include_private = false| super(method_name, include_private) || attribute_definition.matches?(method_name) end @@ -56,6 +56,8 @@ module ActiveModel klass.send(:attr_writer, *attr_writers) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :attributes @@ -93,7 +95,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information. + # See <tt>ActiveModel::Validations#validates</tt> for more information. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index a201f72ed0..4e94422cf1 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -23,7 +23,6 @@ module ActiveModel included do include ActiveSupport::Callbacks define_callbacks :validation, - terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name] end @@ -104,10 +103,10 @@ module ActiveModel end end - protected + private # Overwrite run validations to include callbacks. - def run_validations! #:nodoc: + def run_validations! _run_validation_callbacks { super } end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 33ca6f6946..03585bf5e1 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -69,7 +69,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 82a1893823..b7156ba802 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -38,7 +38,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index e4ea42f8f4..98b3f6a8e5 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/regexp" - module ActiveModel module Validations class FormatValidator < EachValidator # :nodoc: @@ -105,7 +103,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 0ea6012a5d..c6c5bae649 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -36,7 +36,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index c924c8d2f8..940c58f3a7 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,12 +1,10 @@ -require "active_support/core_ext/string/strip" - module ActiveModel module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze - RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] + RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long] def initialize(options) if range = (options.delete(:in) || options.delete(:within)) @@ -18,27 +16,6 @@ module ActiveModel options[:minimum] = 1 end - if options[:tokenizer] - ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) - The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. - You can achieve the same functionality by defining an instance method - with the value that you want to validate the length of. For example, - - validates_length_of :essay, minimum: 100, - tokenizer: ->(str) { str.scan(/\w+/) } - - should be written as - - validates_length_of :words_in_essay, minimum: 100 - - private - - def words_in_essay - essay.scan(/\w+/) - end - EOS - end - super end @@ -59,7 +36,6 @@ module ActiveModel end def validate_each(record, attribute, value) - value = tokenize(record, value) value_length = value.respond_to?(:length) ? value.length : value.to_s.length errors_options = options.except(*RESERVED_OPTIONS) @@ -80,17 +56,6 @@ module ActiveModel end private - def tokenize(record, value) - tokenizer = options[:tokenizer] - if tokenizer && value.kind_of?(String) - if tokenizer.kind_of?(Proc) - tokenizer.call(value) - elsif record.respond_to?(tokenizer) - record.send(tokenizer, value) - end - end || value - end - def skip_nil_check?(key) key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil? end @@ -145,7 +110,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 48e0e5c9f6..fb053a4c4e 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -36,7 +36,9 @@ module ActiveModel return end - unless raw_value.is_a?(Numeric) + if raw_value.is_a?(Numeric) + value = raw_value + else value = parse_raw_value_as_a_number(raw_value) end @@ -61,7 +63,7 @@ module ActiveModel end end - protected + private def is_number?(raw_value) !parse_raw_value_as_a_number(raw_value).nil? @@ -70,6 +72,7 @@ module ActiveModel end def parse_raw_value_as_a_number(raw_value) + return raw_value.to_i if is_integer?(raw_value) Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ end @@ -94,18 +97,16 @@ module ActiveModel end end - private - - def record_attribute_changed_in_place?(record, attr_name) - record.respond_to?(:attribute_changed_in_place?) && - record.attribute_changed_in_place?(attr_name.to_s) - end + def record_attribute_changed_in_place?(record, attr_name) + record.respond_to?(:attribute_changed_in_place?) && + record.attribute_changed_in_place?(attr_name.to_s) + end end module HelperMethods # Validates whether the value of the specified attribute is numeric by # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt> - # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt> + # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt> # (if <tt>only_integer</tt> is set to +true+). # # class Person < ActiveRecord::Base @@ -136,7 +137,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ . - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information # # The following checks can also be supplied with a proc or a symbol which # corresponds to a method: diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 0c11cf4265..ce4106a5e1 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,4 +1,3 @@ - module ActiveModel module Validations class PresenceValidator < EachValidator # :nodoc: @@ -29,7 +28,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index c3cbb6e291..a8b958e974 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -18,7 +18,6 @@ module ActiveModel # validates :first_name, length: { maximum: 30 } # validates :age, numericality: true # validates :username, presence: true - # validates :username, uniqueness: true # # The power of the +validates+ method comes when using custom validators # and default validators in one call for a given attribute. @@ -34,7 +33,7 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :name, :email # - # validates :name, presence: true, uniqueness: true, length: { maximum: 100 } + # validates :name, presence: true, length: { maximum: 100 } # validates :email, presence: true, email: true # end # @@ -72,7 +71,7 @@ module ActiveModel # There is also a list of options that could be used along with validators: # # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) @@ -94,7 +93,7 @@ module ActiveModel # Example: # # validates :password, presence: true, confirmation: true, if: :password_required? - # validates :token, uniqueness: true, strict: TokenGenerationException + # validates :token, length: 24, strict: TokenLengthException # # # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+ @@ -148,15 +147,15 @@ module ActiveModel validates(*(attributes << options)) end - protected + private # 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 # :nodoc: + def _validates_default_keys [:if, :unless, :on, :allow_blank, :allow_nil , :strict] end - def _parse_validates_options(options) # :nodoc: + def _parse_validates_options(options) case options when TrueClass {} diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 6de01b3392..9227dd06ff 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/array/extract_options" + module ActiveModel module Validations class WithValidator < EachValidator # :nodoc: @@ -43,7 +45,7 @@ module ActiveModel # # Configuration options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) @@ -59,7 +61,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a +true+ or # +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # See <tt>ActiveModel::Validations#validates!</tt> for more information. # # If you pass any additional configuration options, they will be passed # to the class and available as +options+: diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 9c6065f61f..6c11981e1d 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -82,7 +82,7 @@ module ActiveModel # end # # It can be useful to access the class that is using that validator when there are prerequisites such - # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor. + # as an +attr_accessor+ being present. This class is accessible via <tt>options[:class]</tt> in the constructor. # To setup your validator override the constructor. # # class MyValidator < ActiveModel::Validator @@ -97,20 +97,20 @@ module ActiveModel # Returns the kind of the validator. # # PresenceValidator.kind # => :presence - # UniquenessValidator.kind # => :uniqueness + # AcceptanceValidator.kind # => :acceptance def self.kind @kind ||= name.split("::").last.underscore.chomp("_validator").to_sym unless anonymous? end # Accepts options that will be made available through the +options+ reader. def initialize(options = {}) - @options = options.except(:class).freeze + @options = options.except(:class).freeze end # Returns the kind for this validator. # - # PresenceValidator.new.kind # => :presence - # UniquenessValidator.new.kind # => :uniqueness + # PresenceValidator.new(attributes: [:username]).kind # => :presence + # AcceptanceValidator.new(attributes: [:terms]).kind # => :acceptance def kind self.class.kind end @@ -141,8 +141,8 @@ module ActiveModel end # Performs validation on the supplied record. By default this will call - # +validates_each+ to determine validity therefore subclasses should - # override +validates_each+ with validation logic. + # +validate_each+ to determine validity therefore subclasses should + # override +validate_each+ with validation logic. def validate(record) attributes.each do |attribute| value = record.read_attribute_for_validation(attribute) diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 8eb389d331..fa41d1b95f 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -16,6 +16,8 @@ class AttributeAssignmentTest < ActiveModel::TestCase raise ErrorFromAttributeWriter end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_writer :metadata diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 63b6c56f8c..f85cd7dec4 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -27,7 +27,7 @@ class CallbacksTest < ActiveModel::TestCase false end - ActiveSupport::Deprecation.silence { after_create "@callbacks << :final_callback" } + after_create { |model| model.callbacks << :final_callback } def initialize(options = {}) @callbacks = [] @@ -63,12 +63,10 @@ class CallbacksTest < ActiveModel::TestCase assert_equal model.callbacks.last, :final_callback end - test "the callback chain is halted when a before callback returns false (deprecated)" do + test "the callback chain is not halted when a before callback returns false)" do model = ModelCallbacks.new(before_create_returns: false) - assert_deprecated do - model.create - assert_equal model.callbacks.last, :before_create - end + model.create + assert_equal model.callbacks.last, :final_callback end test "the callback chain is halted when a callback throws :abort" do @@ -127,6 +125,7 @@ class CallbacksTest < ActiveModel::TestCase test "after_create callbacks with both callbacks declared in one line" do assert_equal ["callback1", "callback2"], Violin1.new.create.history end + test "after_create callbacks with both callbacks declared in different lines" do assert_equal ["callback1", "callback2"], Violin2.new.create.history end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 3288b5543c..43aee5a814 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -1,4 +1,6 @@ require "cases/helper" +require "active_support/core_ext/string/strip" +require "yaml" class ErrorsTest < ActiveModel::TestCase class Person @@ -30,7 +32,7 @@ class ErrorsTest < ActiveModel::TestCase def test_delete errors = ActiveModel::Errors.new(self) errors[:foo] << "omg" - errors.delete(:foo) + errors.delete("foo") assert_empty errors[:foo] end @@ -38,6 +40,7 @@ class ErrorsTest < ActiveModel::TestCase errors = ActiveModel::Errors.new(self) errors[:foo] << "omg" assert_includes errors, :foo, "errors should include :foo" + assert_includes errors, "foo", "errors should include 'foo' as :foo" end def test_dup @@ -52,6 +55,7 @@ class ErrorsTest < ActiveModel::TestCase errors = ActiveModel::Errors.new(self) errors[:foo] << "omg" assert_equal true, errors.has_key?(:foo), "errors should have key :foo" + assert_equal true, errors.has_key?("foo"), "errors should have key 'foo' as :foo" end def test_has_no_key @@ -63,6 +67,7 @@ class ErrorsTest < ActiveModel::TestCase errors = ActiveModel::Errors.new(self) errors[:foo] << "omg" assert_equal true, errors.key?(:foo), "errors should have key :foo" + assert_equal true, errors.key?("foo"), "errors should have key 'foo' as :foo" end def test_no_key @@ -79,24 +84,6 @@ class ErrorsTest < ActiveModel::TestCase assert person.errors.empty? end - test "get returns the errors for the provided key" do - errors = ActiveModel::Errors.new(self) - errors[:foo] << "omg" - - assert_deprecated do - assert_equal ["omg"], errors.get(:foo) - end - end - - test "sets the error with the provided key" do - errors = ActiveModel::Errors.new(self) - assert_deprecated do - errors.set(:foo, "omg") - end - - assert_equal({ foo: "omg" }, errors.messages) - end - test "error access is indifferent" do errors = ActiveModel::Errors.new(self) errors[:foo] << "omg" @@ -112,6 +99,14 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["omg", "zomg"], errors.values end + test "values returns an empty array after try to get a message only" do + errors = ActiveModel::Errors.new(self) + errors.messages[:foo] + errors.messages[:baz] + + assert_equal [], errors.values + end + test "keys returns the error keys" do errors = ActiveModel::Errors.new(self) errors.messages[:foo] << "omg" @@ -120,6 +115,14 @@ class ErrorsTest < ActiveModel::TestCase assert_equal [:foo, :baz], errors.keys end + test "keys returns an empty array after try to get a message only" do + errors = ActiveModel::Errors.new(self) + errors.messages[:foo] + errors.messages[:baz] + + assert_equal [], errors.keys + end + test "detecting whether there are errors with empty?, blank?, include?" do person = Person.new person.errors[:foo] @@ -142,14 +145,6 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["cannot be nil"], person.errors[:name] end - test "assign error" do - person = Person.new - assert_deprecated do - person.errors[:name] = "should not be nil" - end - assert_equal ["should not be nil"], person.errors[:name] - end - test "add an error message on a specific attribute" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -176,10 +171,11 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["cannot be blank"], person.errors[:name] end - test "added? detects if a specific error was added to the object" do + test "added? detects indifferent if a specific error was added to the object" do person = Person.new person.errors.add(:name, "cannot be blank") assert person.errors.added?(:name, "cannot be blank") + assert person.errors.added?("name", "cannot be blank") end test "added? handles symbol message" do @@ -250,6 +246,16 @@ class ErrorsTest < ActiveModel::TestCase assert_equal({ name: ["cannot be blank"] }, person.errors.to_hash) end + test "to_hash returns a hash without default proc" do + person = Person.new + assert_nil person.errors.to_hash.default_proc + end + + test "as_json returns a hash without default proc" do + person = Person.new + assert_nil person.errors.as_json.default_proc + end + test "full_messages creates a list of error messages with the attribute name included" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -257,7 +263,7 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages end - test "full_messages_for contains all the error messages for the given attribute" do + test "full_messages_for contains all the error messages for the given attribute indifferent" do person = Person.new person.errors.add(:name, "cannot be blank") person.errors.add(:name, "cannot be nil") @@ -269,6 +275,7 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "cannot be blank") person.errors.add(:email, "cannot be blank") assert_equal ["name cannot be blank"], person.errors.full_messages_for(:name) + assert_equal ["name cannot be blank"], person.errors.full_messages_for("name") end test "full_messages_for returns an empty list in case there are no errors for the given attribute" do @@ -310,72 +317,6 @@ class ErrorsTest < ActiveModel::TestCase } end - test "add_on_empty generates message" do - person = Person.new - assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do - assert_deprecated do - person.errors.add_on_empty :name - end - end - end - - test "add_on_empty generates message for multiple attributes" do - person = Person.new - expected_calls = [ [:name, :empty, {}], [:age, :empty, {}] ] - assert_called_with(person.errors, :generate_message, expected_calls) do - assert_deprecated do - person.errors.add_on_empty [:name, :age] - end - end - end - - test "add_on_empty generates message with custom default message" do - person = Person.new - assert_called_with(person.errors, :generate_message, [:name, :empty, { message: "custom" }]) do - assert_deprecated do - person.errors.add_on_empty :name, message: "custom" - end - end - end - - test "add_on_empty generates message with empty string value" do - person = Person.new - person.name = "" - assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do - assert_deprecated do - person.errors.add_on_empty :name - end - end - end - - test "add_on_blank generates message" do - person = Person.new - assert_called_with(person.errors, :generate_message, [:name, :blank, {}]) do - assert_deprecated do - person.errors.add_on_blank :name - end - end - end - - test "add_on_blank generates message for multiple attributes" do - person = Person.new - expected_calls = [ [:name, :blank, {}], [:age, :blank, {}] ] - assert_called_with(person.errors, :generate_message, expected_calls) do - assert_deprecated do - person.errors.add_on_blank [:name, :age] - end - end - end - - test "add_on_blank generates message with custom default message" do - person = Person.new - assert_called_with(person.errors, :generate_message, [:name, :blank, { message: "custom" }]) do - assert_deprecated do - person.errors.add_on_blank :name, message: "custom" - end - end - end - test "details returns added error detail" do person = Person.new person.errors.add(:name, :invalid) @@ -442,4 +383,24 @@ class ErrorsTest < ActiveModel::TestCase assert_equal errors.messages, serialized.messages assert_equal errors.details, serialized.details end + + test "errors are backward compatible with the Rails 4.2 format" do + yaml = <<-CODE.strip_heredoc + --- !ruby/object:ActiveModel::Errors + base: &1 !ruby/object:ErrorsTest::Person + errors: !ruby/object:ActiveModel::Errors + base: *1 + messages: {} + messages: {} + CODE + + errors = YAML.load(yaml) + errors.add(:name, :invalid) + assert_equal({ name: ["is invalid"] }, errors.messages) + assert_equal({ name: [{ error: :invalid }] }, errors.details) + + errors.clear + assert_equal({}, errors.messages) + assert_equal({}, errors.details) + end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 4e9f43ad86..eeb5c85a48 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -9,15 +9,15 @@ I18n.enforce_available_locales = false require "active_support/testing/autorun" require "active_support/testing/method_call_assertions" -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - -class ActiveModel::TestCase +class ActiveModel::TestCase < ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 9423df2c09..77ce86f392 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -179,7 +179,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "setting a nil password should clear an existing password" do @existing_user.password = nil - assert_equal nil, @existing_user.password_digest + assert_nil @existing_user.password_digest end test "authenticate" do diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 5ee53285a3..f78efd2f0c 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -51,32 +51,32 @@ class SerializationTest < ActiveModel::TestCase end def test_method_serializable_hash_should_work - expected = { "name"=>"David", "gender"=>"male", "email"=>"david@example.com" } + expected = { "name" => "David", "gender" => "male", "email" => "david@example.com" } assert_equal expected, @user.serializable_hash end def test_method_serializable_hash_should_work_with_only_option - expected = { "name"=>"David" } + expected = { "name" => "David" } assert_equal expected, @user.serializable_hash(only: [:name]) end def test_method_serializable_hash_should_work_with_except_option - expected = { "gender"=>"male", "email"=>"david@example.com" } + expected = { "gender" => "male", "email" => "david@example.com" } assert_equal expected, @user.serializable_hash(except: [:name]) end def test_method_serializable_hash_should_work_with_methods_option - expected = { "name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar", "email"=>"david@example.com" } + expected = { "name" => "David", "gender" => "male", "foo" => "i_am_foo", "bar" => "i_am_bar", "email" => "david@example.com" } assert_equal expected, @user.serializable_hash(methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_only_and_methods - expected = { "foo"=>"i_am_foo", "bar"=>"i_am_bar" } + expected = { "foo" => "i_am_foo", "bar" => "i_am_bar" } assert_equal expected, @user.serializable_hash(only: [], methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_except_and_methods - expected = { "gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar" } + expected = { "gender" => "male", "foo" => "i_am_foo", "bar" => "i_am_bar" } assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo, :bar]) end @@ -94,21 +94,21 @@ class SerializationTest < ActiveModel::TestCase end def test_include_option_with_singular_association - expected = { "name"=>"David", "gender"=>"male", "email"=>"david@example.com", - "address"=>{ "street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111 } } + expected = { "name" => "David", "gender" => "male", "email" => "david@example.com", + "address" => { "street" => "123 Lane", "city" => "Springfield", "state" => "CA", "zip" => 11111 } } assert_equal expected, @user.serializable_hash(include: :address) end def test_include_option_with_plural_association - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", - "friends"=>[{ "name"=>"Joe", "email"=>"joe@example.com", "gender"=>"male" }, - { "name"=>"Sue", "email"=>"sue@example.com", "gender"=>"female" }] } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" }, + { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] } assert_equal expected, @user.serializable_hash(include: :friends) end def test_include_option_with_empty_association @user.friends = [] - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", "friends"=>[] } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", "friends" => [] } assert_equal expected, @user.serializable_hash(include: :friends) end @@ -124,52 +124,52 @@ class SerializationTest < ActiveModel::TestCase def test_include_option_with_ary @user.friends = FriendList.new(@user.friends) - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", - "friends"=>[{ "name"=>"Joe", "email"=>"joe@example.com", "gender"=>"male" }, - { "name"=>"Sue", "email"=>"sue@example.com", "gender"=>"female" }] } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" }, + { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] } assert_equal expected, @user.serializable_hash(include: :friends) end def test_multiple_includes - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", - "address"=>{ "street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111 }, - "friends"=>[{ "name"=>"Joe", "email"=>"joe@example.com", "gender"=>"male" }, - { "name"=>"Sue", "email"=>"sue@example.com", "gender"=>"female" }] } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "address" => { "street" => "123 Lane", "city" => "Springfield", "state" => "CA", "zip" => 11111 }, + "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" }, + { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] } assert_equal expected, @user.serializable_hash(include: [:address, :friends]) end def test_include_with_options - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", - "address"=>{ "street"=>"123 Lane" } } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "address" => { "street" => "123 Lane" } } assert_equal expected, @user.serializable_hash(include: { address: { only: "street" } }) end def test_nested_include @user.friends.first.friends = [@user] - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", - "friends"=>[{ "name"=>"Joe", "email"=>"joe@example.com", "gender"=>"male", - "friends"=> [{ "email"=>"david@example.com", "gender"=>"male", "name"=>"David" }] }, - { "name"=>"Sue", "email"=>"sue@example.com", "gender"=>"female", "friends"=> [] }] } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male", + "friends" => [{ "email" => "david@example.com", "gender" => "male", "name" => "David" }] }, + { "name" => "Sue", "email" => "sue@example.com", "gender" => "female", "friends" => [] }] } assert_equal expected, @user.serializable_hash(include: { friends: { include: :friends } }) end def test_only_include - expected = { "name"=>"David", "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] } + expected = { "name" => "David", "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] } assert_equal expected, @user.serializable_hash(only: :name, include: { friends: { only: :name } }) end def test_except_include - expected = { "name"=>"David", "email"=>"david@example.com", - "friends"=> [{ "name" => "Joe", "email" => "joe@example.com" }, + expected = { "name" => "David", "email" => "david@example.com", + "friends" => [{ "name" => "Joe", "email" => "joe@example.com" }, { "name" => "Sue", "email" => "sue@example.com" }] } assert_equal expected, @user.serializable_hash(except: :gender, include: { friends: { except: :gender } }) end def test_multiple_includes_with_options - expected = { "email"=>"david@example.com", "gender"=>"male", "name"=>"David", - "address"=>{ "street"=>"123 Lane" }, - "friends"=>[{ "name"=>"Joe", "email"=>"joe@example.com", "gender"=>"male" }, - { "name"=>"Sue", "email"=>"sue@example.com", "gender"=>"female" }] } + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "address" => { "street" => "123 Lane" }, + "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" }, + { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] } assert_equal expected, @user.serializable_hash(include: [{ address: { only: "street" } }, :friends]) end end diff --git a/activemodel/test/cases/type/big_integer_test.rb b/activemodel/test/cases/type/big_integer_test.rb new file mode 100644 index 0000000000..56002b7cc6 --- /dev/null +++ b/activemodel/test/cases/type/big_integer_test.rb @@ -0,0 +1,24 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class BigIntegerTest < ActiveModel::TestCase + def test_type_cast_big_integer + type = Type::BigInteger.new + assert_equal 1, type.cast(1) + assert_equal 1, type.cast("1") + end + + def test_small_values + type = Type::BigInteger.new + assert_equal(-9999999999999999999999999999999, type.serialize(-9999999999999999999999999999999)) + end + + def test_large_values + type = Type::BigInteger.new + assert_equal 9999999999999999999999999999999, type.serialize(9999999999999999999999999999999) + end + end + end +end diff --git a/activemodel/test/cases/type/binary_test.rb b/activemodel/test/cases/type/binary_test.rb new file mode 100644 index 0000000000..e9c2ccfca4 --- /dev/null +++ b/activemodel/test/cases/type/binary_test.rb @@ -0,0 +1,15 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class BinaryTest < ActiveModel::TestCase + def test_type_cast_binary + type = Type::Binary.new + assert_nil type.cast(nil) + assert_equal "1", type.cast("1") + assert_equal 1, type.cast(1) + end + end + end +end diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb new file mode 100644 index 0000000000..92e5aebfb7 --- /dev/null +++ b/activemodel/test/cases/type/boolean_test.rb @@ -0,0 +1,39 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class BooleanTest < ActiveModel::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.cast("").nil? + assert type.cast(nil).nil? + + assert type.cast(true) + assert type.cast(1) + assert type.cast("1") + assert type.cast("t") + assert type.cast("T") + assert type.cast("true") + assert type.cast("TRUE") + assert type.cast("on") + assert type.cast("ON") + assert type.cast(" ") + assert type.cast("\u3000\r\n") + assert type.cast("\u0000") + assert type.cast("SOMETHING RANDOM") + + # explicitly check for false vs nil + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast("0") + assert_equal false, type.cast("f") + assert_equal false, type.cast("F") + assert_equal false, type.cast("false") + assert_equal false, type.cast("FALSE") + assert_equal false, type.cast("off") + assert_equal false, type.cast("OFF") + end + end + end +end diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb new file mode 100644 index 0000000000..0cc90e99d3 --- /dev/null +++ b/activemodel/test/cases/type/date_test.rb @@ -0,0 +1,19 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class DateTest < ActiveModel::TestCase + def test_type_cast_date + type = Type::Date.new + assert_nil type.cast(nil) + assert_nil type.cast("") + assert_nil type.cast(" ") + assert_nil type.cast("ABC") + + date_string = ::Time.now.utc.strftime("%F") + assert_equal date_string, type.cast(date_string).strftime("%F") + end + end + end +end diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb new file mode 100644 index 0000000000..75a7fc686e --- /dev/null +++ b/activemodel/test/cases/type/date_time_test.rb @@ -0,0 +1,38 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class DateTimeTest < ActiveModel::TestCase + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_nil type.cast(nil) + assert_nil type.cast("") + assert_nil type.cast(" ") + assert_nil type.cast("ABC") + + datetime_string = ::Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") + end + + def test_string_to_time_with_timezone + ["UTC", "US/Eastern"].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal ::Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + private + + def with_timezone_config(default:) + old_zone_default = ::Time.zone_default + ::Time.zone_default = ::Time.find_zone(default) + yield + ensure + ::Time.zone_default = old_zone_default + end + end + end +end diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index 46a913258e..c3b43725cc 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -11,6 +11,14 @@ module ActiveModel assert_equal BigDecimal.new("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") + 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) diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb new file mode 100644 index 0000000000..8026d63ad5 --- /dev/null +++ b/activemodel/test/cases/type/float_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class FloatTest < ActiveModel::TestCase + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.cast("1") + end + + def test_type_cast_float_from_invalid_string + type = Type::Float.new + assert_nil type.cast("") + assert_equal 1.0, type.cast("1ignore") + assert_equal 0.0, type.cast("bad1") + assert_equal 0.0, type.cast("bad") + end + + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, "5wibble") + assert_not type.changed?(5.0, 5.0, "5") + assert_not type.changed?(5.0, 5.0, "5.0") + assert_not type.changed?(nil, nil, nil) + end + end + end +end diff --git a/activemodel/test/cases/type/immutable_string_test.rb b/activemodel/test/cases/type/immutable_string_test.rb new file mode 100644 index 0000000000..23e58974fb --- /dev/null +++ b/activemodel/test/cases/type/immutable_string_test.rb @@ -0,0 +1,21 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class ImmutableStringTest < ActiveModel::TestCase + test "cast strings are frozen" do + s = "foo" + type = Type::ImmutableString.new + assert_equal true, type.cast(s).frozen? + end + + test "immutable strings are not duped coming out" do + s = "foo" + type = Type::ImmutableString.new + assert_same s, type.cast(s) + assert_same s, type.deserialize(s) + end + end + end +end diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb index e00246b602..a91144036b 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -1,11 +1,13 @@ require "cases/helper" require "active_model/type" +require "active_support/core_ext/numeric/time" module ActiveModel module Type class IntegerTest < ActiveModel::TestCase test "simple values" do type = Type::Integer.new + assert_nil type.cast("") assert_equal 1, type.cast(1) assert_equal 1, type.cast("1") assert_equal 1, type.cast("1ignore") @@ -19,7 +21,7 @@ module ActiveModel test "random objects cast to nil" do type = Type::Integer.new - assert_nil type.cast([1,2]) + assert_nil type.cast([1, 2]) assert_nil type.cast(1 => 2) assert_nil type.cast(1..2) end @@ -32,7 +34,7 @@ module ActiveModel test "casting nan and infinity" do type = Type::Integer.new assert_nil type.cast(::Float::NAN) - assert_nil type.cast(1.0/0.0) + assert_nil type.cast(1.0 / 0.0) end test "casting booleans for database" do @@ -41,6 +43,12 @@ module ActiveModel assert_equal 0, type.serialize(false) end + test "casting duration" do + type = Type::Integer.new + assert_equal 1800, type.cast(30.minutes) + assert_equal 7200, type.cast(2.hours) + end + test "changed?" do type = Type::Integer.new diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb index 2a48998a62..927b6d0307 100644 --- a/activemodel/test/cases/type/registry_test.rb +++ b/activemodel/test/cases/type/registry_test.rb @@ -2,38 +2,40 @@ require "cases/helper" require "active_model/type" module ActiveModel - class RegistryTest < ActiveModel::TestCase - test "a class can be registered for a symbol" do - registry = Type::Registry.new - registry.register(:foo, ::String) - registry.register(:bar, ::Array) + module Type + class RegistryTest < ActiveModel::TestCase + test "a class can be registered for a symbol" do + registry = Type::Registry.new + registry.register(:foo, ::String) + registry.register(:bar, ::Array) - assert_equal "", registry.lookup(:foo) - assert_equal [], registry.lookup(:bar) - end - - test "a block can be registered" do - registry = Type::Registry.new - registry.register(:foo) do |*args| - [*args, "block for foo"] + assert_equal "", registry.lookup(:foo) + assert_equal [], registry.lookup(:bar) end - registry.register(:bar) do |*args| - [*args, "block for bar"] + + test "a block can be registered" do + registry = Type::Registry.new + registry.register(:foo) do |*args| + [*args, "block for foo"] + end + registry.register(:bar) do |*args| + [*args, "block for bar"] + end + + assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1) + assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2) + assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3) end - assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1) - assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2) - assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3) - end + test "a reasonable error is given when no type is found" do + registry = Type::Registry.new - test "a reasonable error is given when no type is found" do - registry = Type::Registry.new + e = assert_raises(ArgumentError) do + registry.lookup(:foo) + end - e = assert_raises(ArgumentError) do - registry.lookup(:foo) + assert_equal "Unknown type :foo", e.message end - - assert_equal "Unknown type :foo", e.message end end end diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb index 7b25a1ef74..47d412e27e 100644 --- a/activemodel/test/cases/type/string_test.rb +++ b/activemodel/test/cases/type/string_test.rb @@ -2,26 +2,36 @@ require "cases/helper" require "active_model/type" module ActiveModel - class StringTypeTest < ActiveModel::TestCase - test "type casting" do - type = Type::String.new - assert_equal "t", type.cast(true) - assert_equal "f", type.cast(false) - assert_equal "123", type.cast(123) - end + module Type + class StringTest < ActiveModel::TestCase + test "type casting" do + type = Type::String.new + assert_equal "t", type.cast(true) + assert_equal "f", type.cast(false) + assert_equal "123", type.cast(123) + end - test "immutable strings are not duped coming out" do - s = "foo" - type = Type::ImmutableString.new - assert_same s, type.cast(s) - assert_same s, type.deserialize(s) - end + test "cast strings are mutable" do + type = Type::String.new + + s = "foo" + assert_equal false, type.cast(s).frozen? + assert_equal false, s.frozen? + + f = "foo".freeze + assert_equal false, type.cast(f).frozen? + assert_equal true, f.frozen? + end + + test "values are duped coming out" do + type = Type::String.new - test "values are duped coming out" do - s = "foo" - type = Type::String.new - assert_not_same s, type.cast(s) - assert_not_same s, type.deserialize(s) + s = "foo" + assert_not_same s, type.cast(s) + assert_equal s, type.cast(s) + assert_not_same s, type.deserialize(s) + assert_equal s, type.deserialize(s) + end end end end diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb new file mode 100644 index 0000000000..0cc4d33caa --- /dev/null +++ b/activemodel/test/cases/type/time_test.rb @@ -0,0 +1,21 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class TimeTest < ActiveModel::TestCase + def test_type_cast_time + type = Type::Time.new + assert_nil type.cast(nil) + assert_nil type.cast("") + assert_nil type.cast("ABC") + + time_string = ::Time.now.utc.strftime("%T") + assert_equal time_string, type.cast(time_string).strftime("%T") + + assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00") + assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00") + end + end + end +end diff --git a/activemodel/test/cases/type/value_test.rb b/activemodel/test/cases/type/value_test.rb new file mode 100644 index 0000000000..d8b3e7f164 --- /dev/null +++ b/activemodel/test/cases/type/value_test.rb @@ -0,0 +1,14 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class ValueTest < ActiveModel::TestCase + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + end + end +end diff --git a/activemodel/test/cases/types_test.rb b/activemodel/test/cases/types_test.rb deleted file mode 100644 index c46775cb41..0000000000 --- a/activemodel/test/cases/types_test.rb +++ /dev/null @@ -1,125 +0,0 @@ -require "cases/helper" -require "active_model/type" -require "active_support/core_ext/numeric/time" - -module ActiveModel - class TypesTest < ActiveModel::TestCase - def test_type_cast_boolean - type = Type::Boolean.new - assert type.cast("").nil? - assert type.cast(nil).nil? - - assert type.cast(true) - assert type.cast(1) - assert type.cast("1") - assert type.cast("t") - assert type.cast("T") - assert type.cast("true") - assert type.cast("TRUE") - assert type.cast("on") - assert type.cast("ON") - assert type.cast(" ") - assert type.cast("\u3000\r\n") - assert type.cast("\u0000") - assert type.cast("SOMETHING RANDOM") - - # explicitly check for false vs nil - assert_equal false, type.cast(false) - assert_equal false, type.cast(0) - assert_equal false, type.cast("0") - assert_equal false, type.cast("f") - assert_equal false, type.cast("F") - assert_equal false, type.cast("false") - assert_equal false, type.cast("FALSE") - assert_equal false, type.cast("off") - assert_equal false, type.cast("OFF") - end - - def test_type_cast_float - type = Type::Float.new - assert_equal 1.0, type.cast("1") - end - - def test_changing_float - type = Type::Float.new - - assert type.changed?(5.0, 5.0, "5wibble") - assert_not type.changed?(5.0, 5.0, "5") - assert_not type.changed?(5.0, 5.0, "5.0") - assert_not type.changed?(nil, nil, nil) - end - - def test_type_cast_binary - type = Type::Binary.new - assert_equal nil, type.cast(nil) - assert_equal "1", type.cast("1") - assert_equal 1, type.cast(1) - end - - def test_type_cast_time - type = Type::Time.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast("") - assert_equal nil, type.cast("ABC") - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.cast(time_string).strftime("%T") - - assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00") - assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00") - end - - def test_type_cast_datetime_and_timestamp - type = Type::DateTime.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast("") - assert_equal nil, type.cast(" ") - assert_equal nil, type.cast("ABC") - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") - end - - def test_type_cast_date - type = Type::Date.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast("") - assert_equal nil, type.cast(" ") - assert_equal nil, type.cast("ABC") - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - type = Type::Integer.new - assert_equal 1800, type.cast(30.minutes) - assert_equal 7200, type.cast(2.hours) - end - - def test_string_to_time_with_timezone - ["UTC", "US/Eastern"].each do |zone| - with_timezone_config default: zone do - type = Type::DateTime.new - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - def test_type_equality - assert_equal Type::Value.new, Type::Value.new - assert_not_equal Type::Value.new, Type::Integer.new - assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) - end - - private - - def with_timezone_config(default:) - old_zone_default = ::Time.zone_default - ::Time.zone_default = ::Time.find_zone(default) - yield - ensure - ::Time.zone_default = old_zone_default - end - end -end diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index 3e48e591e9..833f694c5a 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -19,7 +19,7 @@ class AbsenceValidationTest < ActiveModel::TestCase assert_equal ["must be blank"], t.errors[:title] assert_equal ["must be blank"], t.errors[:content] t.title = "" - t.content = "something" + t.content = "something" assert t.invalid? assert_equal ["must be blank"], t.errors[:content] assert_equal [], t.errors[:title] diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index 55ab213498..fbd994e914 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -19,7 +19,7 @@ class AcceptanceValidationTest < ActiveModel::TestCase def test_terms_of_service_agreement Topic.validates_acceptance_of(:terms_of_service) - t = Topic.new("title" => "We should be confirmed","terms_of_service" => "") + t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") assert t.invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] @@ -30,7 +30,7 @@ class AcceptanceValidationTest < ActiveModel::TestCase def test_eula Topic.validates_acceptance_of(:eula, message: "must be abided") - t = Topic.new("title" => "We should be confirmed","eula" => "") + t = Topic.new("title" => "We should be confirmed", "eula" => "") assert t.invalid? assert_equal ["must be abided"], t.errors[:eula] diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index 83e8ac9522..f2e4a5946d 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -29,7 +29,7 @@ class DogWithTwoValidators < Dog before_validation { history << "before_validation_marker2" } end -class DogDeprecatedBeforeValidatorReturningFalse < Dog +class DogBeforeValidatorReturningFalse < Dog before_validation { false } before_validation { history << "before_validation_marker2" } end @@ -121,13 +121,11 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase assert_equal false, output end - def test_deprecated_further_callbacks_should_not_be_called_if_before_validation_returns_false - d = DogDeprecatedBeforeValidatorReturningFalse.new - assert_deprecated do - output = d.valid? - assert_equal [], d.history - assert_equal false, output - end + def test_further_callbacks_should_be_called_if_before_validation_returns_false + d = DogBeforeValidatorReturningFalse.new + output = d.valid? + assert_equal ["before_validation_marker2"], d.history + assert_equal true, output end def test_further_callbacks_should_be_called_if_after_validation_returns_false diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 5e81083b63..048d27446e 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -43,7 +43,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -52,7 +54,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert_empty t.errors[:title] @@ -60,7 +64,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert_empty t.errors[:title] @@ -68,7 +74,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -106,7 +114,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_block_false # When the block returns false Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", - unless: Proc.new { |r| r.title != "uhohuhoh" } ) + unless: Proc.new { |r| r.title != "uhohuhoh" }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -118,7 +126,9 @@ class ConditionalValidationTest < ActiveModel::TestCase # ensure that it works correctly def test_validation_with_if_as_string Topic.validates_presence_of(:title) - Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')") + ActiveSupport::Deprecation.silence do + Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')") + end t = Topic.new assert t.invalid?, "A topic without a title should not be valid" diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index b88e1c4ca4..7ddf3ad273 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -28,7 +28,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase def test_title_confirmation Topic.validates_confirmation_of(:title) - t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") + t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") assert t.invalid? t.title_confirmation = "We should be confirmed" @@ -61,7 +61,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title) - t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") + t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") assert t.invalid? assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] ensure diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index d7e6bf3707..f5b1ad721c 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -107,7 +107,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validates_format_of_with_lambda - Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ } + Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\z/ : /\A\S+\z/ } t = Topic.new t.title = "digit" @@ -119,7 +119,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validates_format_of_without_lambda - Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ } + Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\z/ : /\A\S+\z/ } t = Topic.new t.title = "characters" @@ -131,7 +131,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validates_format_of_for_ruby_class - Person.validates_format_of :karma, with: /\A\d+\Z/ + Person.validates_format_of :karma, with: /\A\d+\z/ p = Person.new p.karma = "Pixies" diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index ade185c179..95ee87b401 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -9,7 +9,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_allow_nil - Topic.validates_length_of( :title, is: 5, allow_nil: true ) + Topic.validates_length_of(:title, is: 5, allow_nil: true) assert Topic.new("title" => "ab").invalid? assert Topic.new("title" => "").invalid? @@ -18,7 +18,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_allow_blank - Topic.validates_length_of( :title, is: 5, allow_blank: true ) + Topic.validates_length_of(:title, is: 5, allow_blank: true) assert Topic.new("title" => "ab").invalid? assert Topic.new("title" => "").valid? @@ -104,7 +104,7 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content] t.title = "abe" - t.content = "mad" + t.content = "mad" assert t.valid? end @@ -161,8 +161,8 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_bignum - bigmin = 2 ** 30 - bigmax = 2 ** 32 + bigmin = 2**30 + bigmax = 2**32 bigrange = bigmin...bigmax assert_nothing_raised do Topic.validates_length_of :title, is: bigmin + 5 @@ -183,7 +183,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_minimum_with_message - Topic.validates_length_of( :title, minimum: 5, message: "boo %{count}" ) + 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? @@ -191,7 +191,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_minimum_with_too_short - Topic.validates_length_of( :title, minimum: 5, too_short: "hoo %{count}" ) + 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? @@ -199,7 +199,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_maximum_with_message - Topic.validates_length_of( :title, maximum: 5, message: "boo %{count}" ) + 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? @@ -220,7 +220,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_maximum_with_too_long - Topic.validates_length_of( :title, maximum: 5, too_long: "hoo %{count}" ) + 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? @@ -242,7 +242,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_is_with_message - Topic.validates_length_of( :title, is: 5, message: "boo %{count}" ) + 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? @@ -250,7 +250,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_is_with_wrong_length - Topic.validates_length_of( :title, is: 5, wrong_length: "hoo %{count}" ) + 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? @@ -289,7 +289,7 @@ class LengthValidationTest < ActiveModel::TestCase 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三" + t.content = "12三" assert t.valid? end @@ -318,42 +318,6 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"] end - def test_validates_length_of_with_block - assert_deprecated do - Topic.validates_length_of( - :content, - minimum: 5, - too_short: "Your essay must be at least %{count} words.", - tokenizer: lambda { |str| str.scan(/\w+/) }, - ) - end - t = Topic.new(content: "this content should be long enough") - assert t.valid? - - t.content = "not long enough" - assert t.invalid? - assert t.errors[:content].any? - assert_equal ["Your essay must be at least 5 words."], t.errors[:content] - end - - def test_validates_length_of_with_symbol - assert_deprecated do - Topic.validates_length_of( - :content, - minimum: 5, - too_short: "Your essay must be at least %{count} words.", - tokenizer: :my_word_tokenizer, - ) - end - t = Topic.new(content: "this content should be long enough") - assert t.valid? - - t.content = "not long enough" - assert t.invalid? - assert t.errors[:content].any? - assert_equal ["Your essay must be at least 5 words."], t.errors[:content] - end - def test_validates_length_of_for_integer Topic.validates_length_of(:approved, is: 4) @@ -439,7 +403,7 @@ class LengthValidationTest < ActiveModel::TestCase 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 } ) + Topic.validates_length_of(:title, is: 5, if: Proc.new { false }) assert Topic.new("title" => "david").valid? assert Topic.new("title" => "david2").invalid? diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index fefab29f15..c0158e075f 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 INTEGERS = [0, 10, -10] + INTEGER_STRINGS BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(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] + INFINITY = [1.0 / 0.0] def test_default_validates_numericality_of Topic.validates_numericality_of :approved @@ -82,7 +82,7 @@ class NumericalityValidationTest < ActiveModel::TestCase Topic.validates_numericality_of :approved, greater_than: BigDecimal.new("97.18") invalid!([-97.18, BigDecimal.new("97.18"), BigDecimal("-97.18")], "must be greater than 97.18") - valid!([97.18, 98, BigDecimal.new("98")]) # Notice the 97.18 as a float is greater than 97.18 as a BigDecimal due to floating point precision + valid!([97.19, 98, BigDecimal.new("98"), BigDecimal.new("97.19")]) end def test_validates_numericality_with_greater_than_using_string_value @@ -123,7 +123,7 @@ class NumericalityValidationTest < ActiveModel::TestCase def test_validates_numericality_with_equal_to_using_differing_numeric_types Topic.validates_numericality_of :approved, equal_to: BigDecimal.new("97.18") - invalid!([-97.18, 97.18], "must be equal to 97.18") + invalid!([-97.18], "must be equal to 97.18") valid!([BigDecimal.new("97.18")]) end @@ -165,7 +165,7 @@ class NumericalityValidationTest < ActiveModel::TestCase 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") - invalid!([97.18, 98], "must be less than or equal to 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")]) end @@ -260,6 +260,15 @@ class NumericalityValidationTest < ActiveModel::TestCase Person.clear_validators! end + def test_validates_numericality_with_exponent_number + base = 10_000_000_000_000_000 + Topic.validates_numericality_of :approved, less_than_or_equal_to: base + topic = Topic.new + topic.approved = (base + 1).to_s + + assert topic.invalid? + end + def test_validates_numericality_with_invalid_args assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" } assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" } diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index feda817698..642dd0f144 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -20,7 +20,7 @@ class PresenceValidationTest < ActiveModel::TestCase assert_equal ["can't be blank"], t.errors[:content] t.title = "something" - t.content = " " + t.content = " " assert t.invalid? assert_equal ["can't be blank"], t.errors[:content] diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 20c11dd852..5ce86738cd 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -69,26 +69,34 @@ class ValidatesWithTest < ActiveModel::TestCase end test "with if statements that return false" do - Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2") + end topic = Topic.new assert topic.valid? end test "with if statements that return true" do - Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1") + end topic = Topic.new assert topic.invalid? assert_includes topic.errors[:base], ERROR_MESSAGE end test "with unless statements that return true" do - Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1") + end topic = Topic.new assert topic.valid? end test "with unless statements that returns false" do - Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2") + end topic = Topic.new assert topic.invalid? assert_includes topic.errors[:base], ERROR_MESSAGE @@ -102,7 +110,9 @@ class ValidatesWithTest < ActiveModel::TestCase validator.expect(:is_a?, false, [Symbol]) validator.expect(:is_a?, false, [String]) - Topic.validates_with(validator, if: "1 == 1", foo: :bar) + ActiveSupport::Deprecation.silence do + Topic.validates_with(validator, if: "1 == 1", foo: :bar) + end assert topic.valid? validator.verify end diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index 3924741acc..192786c096 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -36,8 +36,4 @@ class Topic def my_validation_with_arg(attr) errors.add attr, "is missing" unless send(attr) end - - def my_word_tokenizer(str) - str.scan(/\w+/) - end end diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb index 43011b277b..f733c45cf0 100644 --- a/activemodel/test/validators/email_validator.rb +++ b/activemodel/test/validators/email_validator.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/regexp" - class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors[attribute] << (options[:message] || "is not an email") unless diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 4d0c1a4178..1f8163db12 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,242 +1,64 @@ -* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null` +* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. - *Trung Duc Tran* - -* Return `true` from `update_attribute` when the value of the attribute - to be updated is unchanged. - - Fixes #26593. - - *Prathamesh Sonpatki* - -* Always store errors details information with symbols. - - When the association is autosaved we were storing the details with - string keys. This was creating inconsistency with other details that are - added using the `Errors#add` method. It was also inconsistent with the - `Errors#messages` storage. - - To fix this inconsistency we are always storing with symbols. This will - cause a small breaking change because in those cases the details could - be accessed as strings keys but now it can not. - - Fix #26499. - - *Rafael Mendonça França*, *Marcus Vieira* - -* Calling `touch` on a model using optimistic locking will now leave the model - in a non-dirty state with no attribute changes. - - Fixes #26496. - - *Jakob Skjerning* - -* Using a mysql2 connection after it fails to reconnect will now have an error message - saying the connection is closed rather than an undefined method error message. - - *Dylan Thacker-Smith* + *Ryuta Kamizono* -* PostgreSQL array columns will now respect the encoding of strings contained - in the array. +* Loading model schema from database is now thread-safe. - Fixes #26326. + Fixes #28589. - *Sean Griffin* + *Vikrant Chaudhary*, *David Abdemoulaie* -* Inverse association instances will now be set before `after_find` or - `after_initialize` callbacks are run. +* 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. - Fixes #26320. + 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+ - *Sean Griffin* + *DHH* -* Remove unnecessarily association load when a `belongs_to` association has already been - loaded then the foreign key is changed directly and the record saved. +* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump - *James Coleman* + *Rusty Geldmacher*, *Guillermo Iguaran* -* Remove standardized column types/arguments spaces in schema dump. +* Add type caster to `RuntimeReflection#alias_name` - *Tim Petricola* + Fixes #28959. -* Avoid loading records from database when they are already loaded using - the `pluck` method on a collection. + *Jon Moss* - Fixes #25921. +* Deprecate `supports_statement_cache?`. *Ryuta Kamizono* -* Remove text default treated as an empty string in non-strict mode for - consistency with other types. - - Strict mode controls how MySQL handles invalid or missing values in - data-change statements such as INSERT or UPDATE. If strict mode is not - in effect, MySQL inserts adjusted values for invalid or missing values - and produces warnings. +* Quote database name in `db:create` grant statement (when database user does not have access to create the database). - def test_mysql_not_null_defaults_non_strict - using_strict(false) do - with_mysql_not_null_table do |klass| - record = klass.new - assert_nil record.non_null_integer - assert_nil record.non_null_string - assert_nil record.non_null_text - assert_nil record.non_null_blob + *Rune Philosof* - record.save! - record.reload +* Raise error `UnknownMigrationVersionError` on the movement of migrations + when the current migration does not exist. - assert_equal 0, record.non_null_integer - assert_equal "", record.non_null_string - assert_equal "", record.non_null_text - assert_equal "", record.non_null_blob - end - end - end + *bogdanvlviv* - https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict - - *Ryuta Kamizono* +* Fix `bin/rails db:forward` first migration. -* Sqlite3 migrations to add a column to an existing table can now be - successfully rolled back when the column was given and invalid column - type. + *bogdanvlviv* - Fixes #26087 +* Support Descending Indexes for MySQL. - *Travis O'Neill* - -* Deprecate `sanitize_conditions`. Use `sanitize_sql` instead. + 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* -* Doing count on relations that contain LEFT OUTER JOIN Arel node no longer - force a DISTINCT. This solves issues when using count after a left_joins. - - *Maxime Handfield Lapointe* - -* RecordNotFound raised by association.find exposes `id`, `primary_key` and - `model` methods to be consistent with RecordNotFound raised by Record.find. - - *Michel Pigassou* - -* Hashes can once again be passed to setters of `composed_of`, if all of the - mapping methods are methods implemented on `Hash`. - - Fixes #25978. - - *Sean Griffin* - -* Fix the SELECT statement in `#table_comment` for MySQL. - - *Takeshi Akima* - -* Virtual attributes will no longer raise when read on models loaded from the - database - - *Sean Griffin* - -* Support calling the method `merge` in `scope`'s lambda. - - *Yasuhiro Sugino* - -* Fixes multi-parameter attributes conversion with invalid params. - - *Hiroyuki Ishii* - -* Add newline between each migration in `structure.sql`. - - Keeps schema migration inserts as a single commit, but allows for easier - git diffing. +* Fix inconsistency with changed attributes when overriding AR attribute reader. - Fixes #25504. + *bogdanvlviv* - *Grey Baker*, *Norberto Lopes* - -* The flag `error_on_ignored_order_or_limit` has been deprecated in favor of - the current `error_on_ignored_order`. - - *Xavier Noria* - -* Batch processing methods support `limit`: - - Post.limit(10_000).find_each do |post| - # ... - end - - It also works in `find_in_batches` and `in_batches`. - - *Xavier Noria* - -* Using `group` with an attribute that has a custom type will properly cast - the hash keys after calling a calculation method like `count`. - - Fixes #25595. - - *Sean Griffin* - -* Fix the generated `#to_param` method to use `omission: ''` so that - the resulting output is actually up to 20 characters, not - effectively 17 to leave room for the default "...". - Also call `#parameterize` before `#truncate` and make the - `separator: /-/` to maximize the information included in the - output. - - Fixes #23635. - - *Rob Biedenharn* - -* Ensure concurrent invocations of the connection reaper cannot allocate the - same connection to two threads. - - Fixes #25585. - - *Matthew Draper* - -* Inspecting an object with an associated array of over 10 elements no longer - truncates the array, preventing `inspect` from looping infinitely in some - cases. +* 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* -* Removed the unused methods `ActiveRecord::Base.connection_id` and - `ActiveRecord::Base.connection_id=`. - - *Sean Griffin* - -* Ensure hashes can be assigned to attributes created using `composed_of`. - - Fixes #25210. - - *Sean Griffin* - -* Fix logging edge case where if an attribute was of the binary type and - was provided as a Hash. - - *Jon Moss* - -* Handle JSON deserialization correctly if the column default from database - adapter returns `''` instead of `nil`. - - *Johannes Opper* - -* Introduce `ActiveRecord::TransactionSerializationError` for catching - transaction serialization failures or deadlocks. - - *Erol Fornoles* - -* PostgreSQL: Fix `db:structure:load` silent failure on SQL error. - - The command line flag `-v ON_ERROR_STOP=1` should be used - when invoking `psql` to make sure errors are not suppressed. - - Example: - - psql -v ON_ERROR_STOP=1 -q -f awesome-file.sql my-app-db - - Fixes #23818. - - *Ralin Chimev* - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 1f496cf280..f9e4444f07 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/Rakefile b/activerecord/Rakefile index e077d345d6..2d0d5bd657 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,7 +1,7 @@ require "rake/testtask" -require File.expand_path(File.dirname(__FILE__)) + "/test/config" -require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" +require File.expand_path("test/config", __dir__) +require File.expand_path("test/support/config", __dir__) def run_without_aborting(*tasks) errors = [] @@ -50,7 +50,7 @@ end Rake::TestTask.new(adapter => "#{adapter}:env") { |t| adapter_short = adapter == "db2" ? adapter : adapter[/^[a-z0-9]+/] t.libs << "test" - t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { + t.test_files = (Dir.glob("test/cases/**/*_test.rb").reject { |x| x.include?("/adapters/") } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")) @@ -66,7 +66,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 @@ -111,11 +111,6 @@ namespace :db do config = ARTest.config["connections"]["postgresql"] %x( createdb -E UTF8 -T template0 #{config["arunit"]["database"]} ) %x( createdb -E UTF8 -T template0 #{config["arunit2"]["database"]} ) - - # prepare hstore - if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" - puts "Please prepare hstore data type. See http://www.postgresql.org/docs/current/static/hstore.html" - end end desc "Drop the PostgreSQL test databases" @@ -139,7 +134,7 @@ task drop_postgresql_databases: "db:postgresql:drop" task rebuild_postgresql_databases: "db:postgresql:rebuild" task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/active_record/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 2cd8f179dd..450ec0bba9 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "activemodel", version - s.add_dependency "arel", "~> 7.0" + s.add_dependency "arel", "~> 8.0" end diff --git a/activerecord/bin/test b/activerecord/bin/test index 23add35d45..3a9547e5c1 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -17,5 +17,3 @@ module Minitest end Minitest.extensions.unshift "active_record" - -exit Minitest.run(ARGV) diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index f2fe8875b9..3257dd4ad7 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -2,7 +2,7 @@ require "active_record" require "benchmark/ips" TIME = (ENV["BENCHMARK_TIME"] || 20).to_i -RECORDS = (ENV["BENCHMARK_RECORDS"] || TIME*1000).to_i +RECORDS = (ENV["BENCHMARK_RECORDS"] || TIME * 1000).to_i conn = { adapter: "sqlite3", database: ":memory:" } @@ -42,7 +42,7 @@ class Exhibit < ActiveRecord::Base def self.feel(exhibits) exhibits.each(&:feel) end end -def progress_bar(int); print "." if (int%100).zero? ; end +def progress_bar(int); print "." if (int % 100).zero? ; end puts "Generating data..." diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 3b5dab16cb..29f49c6195 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -44,7 +44,6 @@ module ActiveRecord autoload :Explain autoload :Inheritance autoload :Integration - autoload :LegacyYamlAdapter autoload :Migration autoload :Migrator, "active_record/migration" autoload :ModelSchema @@ -85,6 +84,8 @@ module ActiveRecord autoload :AttributeMethods autoload :AutosaveAssociation + autoload :LegacyYamlAdapter + autoload :Relation autoload :AssociationRelation autoload :NullRelation @@ -176,5 +177,5 @@ ActiveSupport.on_load(:active_record) do end ActiveSupport.on_load(:i18n) do - I18n.load_path << File.dirname(__FILE__) + "/active_record/locale/en.yml" + I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__) end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 5ca8fe576e..10cbd5429c 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -15,11 +15,11 @@ module ActiveRecord private - def clear_aggregation_cache # :nodoc: + def clear_aggregation_cache @aggregation_cache.clear if persisted? end - def init_internals # :nodoc: + def init_internals @aggregation_cache = {} super end @@ -206,7 +206,7 @@ module ActiveRecord # or a Proc that is called when a new value is assigned to the value object. The converter is # passed the single value that is used in the assignment and is only called if the new value is # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter - # can return nil to skip the assignment. + # can return +nil+ to skip the assignment. # # Option examples: # composed_of :temperature, mapping: %w(reading celsius) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b5f1f1980a..7c37132d3a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -97,6 +97,16 @@ module ActiveRecord end end + class HasManyThroughOrderError < ActiveRecordError #:nodoc: + def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil) + if owner_class_name && reflection && through_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.") + else + super("Cannot have a has_many :through association before the through association is defined.") + end + end + end + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner = nil, reflection = nil) if owner && reflection @@ -107,6 +117,21 @@ module ActiveRecord end end + class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc: + def initialize(klass, macro, association_name, options, possible_sources) + example_options = options.dup + example_options[:source] = possible_sources.first + + super("Ambiguous source reflection for through association. Please " \ + "specify a :source directive on your declaration like:\n" \ + "\n" \ + " class #{klass} < ActiveRecord::Base\n" \ + " #{macro} :#{association_name}, #{example_options}\n" \ + " end" + ) + end + end + class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: end @@ -224,6 +249,11 @@ module ActiveRecord autoload :AliasTracker end + def self.eager_load! + super + Preloader.eager_load! + end + # Returns the association instance for the given name, instantiating it if it doesn't already exist def association(name) #:nodoc: association = association_instance_get(name) @@ -255,16 +285,16 @@ module ActiveRecord private # Clears out the association cache. - def clear_association_cache # :nodoc: + def clear_association_cache @association_cache.clear if persisted? end - def init_internals # :nodoc: + def init_internals @association_cache = {} super end - # Returns the specified association instance if it exists, nil otherwise. + # Returns the specified association instance if it exists, +nil+ otherwise. def association_instance_get(name) @association_cache[name] end @@ -349,23 +379,23 @@ module ActiveRecord # # === Overriding generated methods # - # Association methods are generated in a module that is included into the model class, - # which allows you to easily override with your own methods and call the original - # generated method with +super+. For example: + # Association methods are generated in a module included into the model + # class, making overrides easy. The original generated method can thus be + # called with +super+: # # class Car < ActiveRecord::Base # belongs_to :owner # belongs_to :old_owner + # # def owner=(new_owner) # self.old_owner = self.owner # super # end # end # - # If your model class is <tt>Project</tt>, then the module is - # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is - # included in the model class immediately after the (anonymous) generated attributes methods - # module, meaning an association will override the methods for an attribute with the same name. + # The association methods module is included immediately after the + # generated attributes methods module, meaning an association will + # override the methods for an attribute with the same name. # # == Cardinality and associations # @@ -1246,7 +1276,7 @@ module ActiveRecord # Scope examples: # has_many :comments, -> { where(author_id: 1) } # has_many :employees, -> { joins(:address) } - # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) } # # === Extensions # @@ -1413,7 +1443,7 @@ module ActiveRecord # Scope examples: # has_one :author, -> { where(comment_id: 1) } # has_one :employer, -> { joins(:company) } - # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) } # # === Options # @@ -1543,7 +1573,7 @@ module ActiveRecord # Scope examples: # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } - # belongs_to :level, ->(level) { where("game_level > ?", level.current) } + # belongs_to :level, ->(game) { where("game_level > ?", game.current_level) } # # === Options # @@ -1617,6 +1647,9 @@ module ActiveRecord # +:inverse_of+ to avoid an extra query during validation. # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If # you don't want to have association presence validated, use <tt>optional: true</tt>. + # [:default] + # Provide a callable (i.e. proc or lambda) to specify that the association should + # be initialized with a particular record before validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1630,6 +1663,7 @@ module ActiveRecord # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at # belongs_to :user, optional: true + # belongs_to :account, default: -> { company.account } def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1735,9 +1769,8 @@ module ActiveRecord # # Scope examples: # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } - # has_and_belongs_to_many :categories, ->(category) { - # where("default_category = ?", category.name) - # } + # has_and_belongs_to_many :categories, ->(post) { + # where("default_category = ?", post.default_category) # # === Extensions # @@ -1797,7 +1830,7 @@ module ActiveRecord builder = Builder::HasAndBelongsToMany.new name, self, options - join_model = builder.through_model + join_model = ActiveSupport::Deprecation.silence { builder.through_model } const_set join_model.name, join_model private_constant join_model.name @@ -1826,8 +1859,8 @@ module ActiveRecord hm_options[k] = options[k] if options.key? k end - has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = habtm_reflection + ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension } + _reflections[name.to_s].parent_reflection = habtm_reflection end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 3963008a76..4a5c821607 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -4,8 +4,6 @@ module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - attr_reader :aliases - def self.create(connection, initial_table, type_caster) aliases = Hash.new(0) aliases[initial_table] = 1 @@ -80,6 +78,11 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :aliases + private def truncate(name) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f506614591..ee2e79889f 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -83,7 +83,7 @@ module ActiveRecord end def scope - target_scope.merge(association_scope) + target_scope.merge!(association_scope) end # The scope for this association. @@ -112,6 +112,15 @@ module ActiveRecord record end + # Remove the inverse association, if possible + def remove_inverse_instance(record) + if invertible_for?(record) + inverse = record.association(inverse_reflection_for(record).name) + inverse.target = nil + inverse.inversed = false + end + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -124,6 +133,16 @@ module ActiveRecord AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end + def extensions + extensions = klass.default_extensions | reflection.extensions + + if scope = reflection.scope + extensions |= klass.unscoped.instance_exec(owner, &scope).extensions + end + + extensions + end + # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, @@ -143,14 +162,6 @@ module ActiveRecord reset end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.instance_exec(record, &sql) - else - sql - end - end - # We can't dump @reflection since it contains the scope proc def marshal_dump ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } @@ -166,7 +177,7 @@ module ActiveRecord def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - assigned_keys = record.changed + assigned_keys = record.changed_attribute_names_to_save assigned_keys += except_from_scope_attributes.keys.map(&:to_s) attributes = create_scope.except(*(assigned_keys - skip_assign)) record.assign_attributes(attributes) @@ -254,7 +265,7 @@ module ActiveRecord # so that when stale_state is different from the value stored on the last find_target, # the target is stale. # - # This is only relevant to certain associations, which is why it returns nil by default. + # This is only relevant to certain associations, which is why it returns +nil+ by default. def stale_state end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 12f8c1ccd4..1593b94f0c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -24,8 +24,8 @@ module ActiveRecord alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster chain_head, chain_tail = get_chain(reflection, association, alias_tracker) - scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) + scope.extending! reflection.extensions + add_constraints(scope, owner, reflection, chain_head, chain_tail) end def join_type @@ -49,6 +49,8 @@ module ActiveRecord binds end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :value_transformation @@ -58,8 +60,8 @@ module ActiveRecord table.create_join(table, table.create_on(constraint), join_type) end - def last_chain_scope(scope, table, reflection, owner, association_klass) - join_keys = reflection.join_keys(association_klass) + def last_chain_scope(scope, table, reflection, owner) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key @@ -78,8 +80,8 @@ module ActiveRecord value_transformation.call(value) end - def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(association_klass) + def next_chain_scope(scope, table, reflection, foreign_table, next_reflection) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key @@ -118,25 +120,25 @@ module ActiveRecord [runtime_reflection, previous_reflection] end - def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) + def add_constraints(scope, owner, refl, chain_head, chain_tail) owner_reflection = chain_tail table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) + scope = last_chain_scope(scope, table, owner_reflection, owner) reflection = chain_head while reflection table = reflection.alias_name + next_reflection = reflection.next unless reflection == chain_tail - next_reflection = reflection.next foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + scope = next_chain_scope(scope, table, reflection, foreign_table, next_reflection) end # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, scope_chain_item, owner) + item = eval_scope(reflection.klass, table, scope_chain_item, owner) if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes) @@ -151,14 +153,15 @@ module ActiveRecord scope.order_values |= item.order_values end - reflection = reflection.next + reflection = next_reflection end scope end - def eval_scope(klass, scope, owner) - klass.unscoped.instance_exec(owner, &scope) + def eval_scope(klass, table, scope, owner) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) + ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 64b2311911..0e61dbfb00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -21,6 +21,10 @@ module ActiveRecord self.target = record end + def default(&block) + writer(owner.instance_exec(&block)) if reader.nil? + end + def reset super @updated = false diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3121e70a04..2b9dd8aae8 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - super + [:polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional, :default] end def self.valid_dependent_options @@ -16,6 +16,7 @@ module ActiveRecord::Associations::Builder # :nodoc: super add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache] add_touch_callbacks(model, reflection) if reflection.options[:touch] + add_default_callbacks(model, reflection) if reflection.options[:default] end def self.define_accessors(mixin, reflection) @@ -35,17 +36,17 @@ module ActiveRecord::Associations::Builder # :nodoc: @_after_create_counter_called = false elsif (@_after_replace_counter_called ||= false) @_after_replace_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? + elsif saved_change_to_attribute?(foreign_key) && !new_record? if reflection.polymorphic? - model = attribute(reflection.foreign_type).try(:constantize) - model_was = attribute_was(reflection.foreign_type).try(:constantize) + model = attribute_in_database(reflection.foreign_type).try(:constantize) + model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) else model = reflection.klass model_was = reflection.klass end - foreign_key_was = attribute_was foreign_key - foreign_key = attribute foreign_key + foreign_key_was = attribute_before_last_save foreign_key + foreign_key = attribute_in_database foreign_key if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) @@ -70,14 +71,16 @@ module ActiveRecord::Associations::Builder # :nodoc: klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: - old_foreign_id = o.changed_attributes[foreign_key] + def self.touch_record(o, changes, foreign_key, name, touch, touch_method) # :nodoc: + old_foreign_id = changes[foreign_key] && changes[foreign_key].first if old_foreign_id association = o.association(name) reflection = association.reflection if reflection.polymorphic? - klass = o.public_send("#{reflection.foreign_type}_was").constantize + foreign_type = reflection.foreign_type + klass = changes[foreign_type] && changes[foreign_type].first || o.public_send(foreign_type) + klass = klass.constantize else klass = association.klass end @@ -107,13 +110,19 @@ module ActiveRecord::Associations::Builder # :nodoc: n = reflection.name touch = reflection.options[:touch] - callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_touch_method) - } + callback = lambda { |changes_method| lambda { |record| + 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) + end - model.after_save callback, if: :changed? - model.after_touch callback - model.after_destroy callback + def self.add_default_callbacks(model, reflection) + model.before_validation lambda { |o| + o.association(reflection.name).default(&reflection.options[:default]) + } end def self.add_destroy_callbacks(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 047292b2bd..6b71826431 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder # :nodoc: class_name = options.fetch(:class_name) { name.to_s.camelize.singularize } - KnownClass.new lhs_class, class_name + KnownClass.new lhs_class, class_name.to_s end end end @@ -78,9 +78,9 @@ module ActiveRecord::Associations::Builder # :nodoc: private - def self.suppress_composite_primary_key(pk) - pk unless pk.is_a?(Array) - end + def self.suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index bb96202a22..7732b63af6 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -8,7 +8,16 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_accessors(model, reflection) super - define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable? + mixin = model.generated_association_methods + name = reflection.name + + define_constructors(mixin, name) if reflection.constructable? + + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def reload_#{name} + association(:#{name}).force_reload_reader + end + CODE end # Defines the (build|create)_association methods for belongs_to or has_one association diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index e5b3af8252..edc53e2517 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -25,26 +25,13 @@ module ActiveRecord # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.items for Foo.has_many :items - def reader(force_reload = false) - if force_reload - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the result collection proxy instead. - MSG - - klass.uncached { reload } - elsif stale_target? + def reader + if stale_target? reload end - if null_scope? - # Cache the proxy separately before the owner has an id - # or else a post-save proxy will still lack the id - @null_proxy ||= CollectionProxy.create(klass, self) - else - @proxy ||= CollectionProxy.create(klass, self) - end + @proxy ||= CollectionProxy.create(klass, self) + @proxy.reset_scope end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -55,9 +42,7 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader if loaded? - load_target.map do |record| - record.send(reflection.association_primary_key) - end + target.pluck(reflection.association_primary_key) else @association_ids ||= ( column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" @@ -68,13 +53,17 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.primary_key_type + pk_type = reflection.association_primary_key_type ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } records = klass.where(reflection.association_primary_key => ids).index_by do |r| r.send(reflection.association_primary_key) - end.values_at(*ids) - replace(records) + end.values_at(*ids).compact + if records.size != ids.size + klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key) + else + replace(records) + end end def reset @@ -192,11 +181,8 @@ module ActiveRecord # +delete_records+. They are in any case removed from the collection. def delete(*records) return if records.empty? - _options = records.extract_options! - dependent = _options[:dependent] || options[:dependent] - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } - delete_or_destroy(records, dependent) + delete_or_destroy(records, options[:dependent]) end # Deletes the +records+ and removes them from this association calling @@ -222,11 +208,7 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.distinct_value - target.uniq.size - else - target.size - end + target.size elsif !association_scope.group_values.empty? load_target.size elsif !association_scope.distinct_value && target.is_a?(Array) @@ -253,13 +235,6 @@ module ActiveRecord end end - def distinct - seen = {} - load_target.find_all do |record| - seen[record.id] = true unless seen.key?(record.id) - end - end - # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) @@ -306,29 +281,9 @@ module ActiveRecord replace_on_target(record, index, skip_callbacks, &block) end - def replace_on_target(record, index, skip_callbacks) - callback(:before_add, record) unless skip_callbacks - - was_loaded = loaded? - yield(record) if block_given? - - unless !was_loaded && loaded? - if index - @target[index] = record - else - @target << record - end - end - - callback(:after_add, record) unless skip_callbacks - set_inverse_instance(record) - - record - end - - def scope(opts = {}) - scope = super() - scope.none! if opts.fetch(:nullify, true) && null_scope? + def scope + scope = super + scope.none! if null_scope? scope end @@ -378,7 +333,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name| mem_record[name] = record[name] end @@ -402,15 +357,19 @@ module ActiveRecord transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? - insert_record(record, true, raise) + insert_record(record, true, raise) { @_was_loaded = loaded? } end end end end # Do the relevant stuff to insert the given record into the association collection. - def insert_record(record, validate = true, raise = false) - raise NotImplementedError + def insert_record(record, validate = true, raise = false, &block) + if raise + record.save!(validate: validate, &block) + else + record.save(validate: validate, &block) + end end def create_scope @@ -438,8 +397,9 @@ module ActiveRecord records.each { |record| callback(:after_remove, record) } end - # Delete the given records from the association, using one of the methods :destroy, - # :delete_all or :nullify (or nil, in which case a default is used). + # Delete the given records from the association, + # using one of the methods +:destroy+, +:delete_all+ + # or +:nullify+ (or +nil+, in which case a default is used). def delete_records(records, method) raise NotImplementedError end @@ -464,19 +424,41 @@ module ActiveRecord end end - def concat_records(records, should_raise = false) + def concat_records(records, raise = false) result = true records.each do |record| raise_on_type_mismatch!(record) - add_to_target(record) do |rec| - result &&= insert_record(rec, true, should_raise) unless owner.new_record? + add_to_target(record) do + result &&= insert_record(record, true, raise) { @_was_loaded = loaded? } unless owner.new_record? end end result && records end + def replace_on_target(record, index, skip_callbacks) + callback(:before_add, record) unless skip_callbacks + + set_inverse_instance(record) + + @_was_loaded = true + + yield(record) if block_given? + + if index + target[index] = record + elsif @_was_loaded || !loaded? + target << record + end + + callback(:after_add, record) unless skip_callbacks + + record + ensure + @_was_loaded = nil + end + def callback(method, record) callbacks_for(method).each do |callback| callback.call(method, owner, record) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index dda240585e..8cdb508c43 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,12 +28,12 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate :exists?, :update_all, :arel, to: :scope - def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table, klass.predicate_builder - merge! association.scope(nullify: false) + + extensions = association.extensions + extend(*extensions) if extensions.any? end def target @@ -81,7 +81,7 @@ module ActiveRecord # # #<Pet id: nil, name: "Choo-Choo"> # # ] # - # person.pets.select(:id, :name ) + # person.pets.select(:id, :name) # # => [ # # #<Pet id: 1, name: "Fancy-Fancy">, # # #<Pet id: 2, name: "Spook">, @@ -106,12 +106,6 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - # - # person.pets.select(:name) { |pet| pet.name =~ /oo/ } - # # => [ - # # #<Pet id: 2, name: "Spook">, - # # #<Pet id: 3, name: "Choo-Choo"> - # # ] # Finds an object in the collection responding to the +id+. Uses the same # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound @@ -724,6 +718,12 @@ module ActiveRecord @association.destroy(*records) end + ## + # :method: distinct + # + # :call-seq: + # distinct(value = true) + # # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -738,11 +738,14 @@ module ActiveRecord # # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def distinct - @association.distinct - end - alias uniq distinct + # + # person.pets.select(:name).distinct.distinct(false) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + #-- def calculate(operation, column_name) null_scope? ? scope.calculate(operation, column_name) : super end @@ -949,19 +952,10 @@ module ActiveRecord @association end - # We don't want this object to be put on the scoping stack, because - # that could create an infinite loop where we call an @association - # method, which gets the current scope, which is this object, which - # delegates to @association, and so on. - def scoping - @association.scope.scoping { yield } - end - # Returns a <tt>Relation</tt> object for the records in this association def scope - @association.scope + @scope ||= @association.scope end - alias spawn scope # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal @@ -1094,7 +1088,7 @@ module ActiveRecord # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload - self + reset_scope end # Unloads the association. Returns +self+. @@ -1116,10 +1110,24 @@ module ActiveRecord def reset proxy_association.reset proxy_association.reset_scope + reset_scope + end + + def reset_scope # :nodoc: + @scope = nil self end - protected + delegate_methods = [ + QueryMethods, + SpawnMethods, + ].flat_map { |klass| + klass.public_instance_methods(false) + } - self.public_instance_methods(false) - [:select] + [:scoping] + + delegate(*delegate_methods, to: :scope) + + private def find_nth_with_limit(index, limit) load_target if find_from_target? @@ -1131,8 +1139,6 @@ module ActiveRecord super end - private - def null_scope? @association.null_scope? end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index d1d0cc4c49..10ca0e47ff 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -16,14 +16,7 @@ module ActiveRecord when :restrict_with_error unless empty? record = owner.class.human_attribute_name(reflection.name).downcase - message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil - if message - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1. - Please use `:'restrict_dependent_destroy.has_many'` instead. - MESSAGE - end - owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record) + owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end @@ -38,13 +31,7 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - set_inverse_instance(record) - - if raise - record.save!(validate: validate) - else - record.save(validate: validate) - end + super end def empty? @@ -72,7 +59,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if reflection.has_cached_counter? - owner._read_attribute reflection.counter_cache_column + owner._read_attribute(reflection.counter_cache_column).to_i else scope.count end @@ -108,7 +95,7 @@ module ActiveRecord end def delete_or_nullify_all_records(method) - count = delete_count(method, self.scope) + count = delete_count(method, scope) update_counter(-count) end 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 d258eac0ed..53ffb3b68d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,10 +38,8 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if raise - record.save!(validate: validate) - else - return unless record.save(validate: validate) + if record.new_record? || record.has_changes_to_save? + return unless super end save_through_record(record) @@ -86,7 +84,10 @@ module ActiveRecord end def save_through_record(record) - build_through_record(record).save! + association = build_through_record(record) + if association.changed? + association.save! + end ensure @through_records.delete(record.object_id) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 5ea9577301..8458253ff8 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,14 +12,7 @@ module ActiveRecord when :restrict_with_error if load_target record = owner.class.human_attribute_name(reflection.name).downcase - message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil - if message - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1. - Please use `:'restrict_dependent_destroy.has_one'` instead. - MESSAGE - end - owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record) + owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end @@ -35,7 +28,7 @@ module ActiveRecord return target unless target || record assigning_another_record = target != record - if assigning_another_record || record.changed? + if assigning_another_record || record.has_changes_to_save? save &&= owner.persisted? transaction_if(save) do @@ -86,12 +79,13 @@ module ActiveRecord target.delete when :destroy target.destroy - else + else nullify_owner_attributes(target) + remove_inverse_instance(target) if target.persisted? && owner.persisted? && !target.save set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \ "The record failed to save after its foreign key was set to nil." end end 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 604904abcc..1183bdf6f4 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -22,6 +22,10 @@ module ActiveRecord elsif record attributes = construct_join_attributes(record) + if through_record && through_record.destroyed? + through_record = through_proxy.tap(&:reload).target + end + if through_record through_record.update(attributes) elsif owner.new_record? diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c26c469c1e..643226267c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -7,12 +7,12 @@ module ActiveRecord class Aliases # :nodoc: def initialize(tables) @tables = tables - @alias_cache = tables.each_with_object({}) { |table,h| - h[table.node] = table.columns.each_with_object({}) { |column,i| + @alias_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns.each_with_object({}) { |column, i| i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table,h| + @name_and_alias_cache = tables.each_with_object({}) { |table, h| h[table.node] = table.columns.map { |column| [column.name, column.alias] } @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) # :nodoc: + Table = Struct.new(:node, :columns) do # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end @@ -62,7 +62,7 @@ module ActiveRecord walk_tree assoc, hash end when Hash - associations.each do |k,v| + associations.each do |k, v| cache = hash[k] ||= {} walk_tree v, cache end @@ -106,12 +106,7 @@ module ActiveRecord def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - - if join_type == Arel::Nodes::OuterJoin - make_left_outer_joins join_root, child - else - make_inner_joins join_root, child - end + make_join_constraints(join_root, child, join_type) } joins.concat outer_joins.flat_map { |oj| @@ -126,8 +121,8 @@ module ActiveRecord end def aliases - Aliases.new join_root.each_with_index.map { |join_part,i| - columns = join_part.column_names.each_with_index.map { |column_name,j| + Aliases.new join_root.each_with_index.map { |join_part, i| + columns = join_part.column_names.each_with_index.map { |column_name, j| Aliases::Column.new column_name, "t#{i}_r#{j}" } Aliases::Table.new(join_part, columns) @@ -143,7 +138,7 @@ module ActiveRecord } } - model_cache = Hash.new { |h,klass| h[klass] = {} } + model_cache = Hash.new { |h, klass| h[klass] = {} } parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root @@ -171,31 +166,19 @@ module ActiveRecord chain = child.reflection.chain foreign_table = parent.table foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) + child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain) end def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end - - def make_left_outer_joins(parent, child) - tables = child.tables join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + make_join_constraints(parent, child, join_type, true) end - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + def make_join_constraints(parent, child, join_type, aliasing = false) + tables = aliasing ? table_aliases_for(parent, child) : child.tables + info = make_constraints(parent, child, tables, join_type) - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } end def table_aliases_for(parent, node) @@ -223,8 +206,8 @@ module ActiveRecord [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - ojs = missing.flat_map { |_,n| make_outer_joins left, n } - intersection.flat_map { |l,r| walk l, r }.concat ojs + ojs = missing.flat_map { |_, n| make_outer_joins left, n } + intersection.flat_map { |l, r| walk l, r }.concat ojs end def find_reflection(klass, name) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index a5705951f3..97cfec0302 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -23,50 +23,27 @@ module ActiveRecord JoinInformation = Struct.new :joins, :binds - def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) + def join_constraints(foreign_table, foreign_klass, join_type, tables, chain) joins = [] binds = [] tables = tables.reverse - scope_chain_index = 0 - scope_chain = scope_chain.reverse - # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse chain.reverse_each do |reflection| table = tables.shift klass = reflection.klass - join_keys = reflection.join_keys(klass) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key constraint = build_constraint(klass, table, key, foreign_table, foreign_key) predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - scope_chain_items = scope_chain[scope_chain_index].map do |item| - if item.is_a?(Relation) - item - else - ActiveRecord::Relation.create(klass, table, predicate_builder) - .instance_exec(node, &item) - end - end - scope_chain_index += 1 - - klass_scope = - if klass.current_scope - klass.current_scope.clone.tap { |scope| - scope.joins_values = [] - } - else - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - klass.send(:build_default_scope, relation) - end + scope_chain_items = reflection.join_scopes(table, predicate_builder) + klass_scope = reflection.klass_join_scope(table, predicate_builder) + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 61cec5403a..80c9fde5d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -22,10 +22,6 @@ module ActiveRecord @children = children end - def name - reflection.name - end - def match?(other) self.class == other.class end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index c79efca920..63ef3f2d8c 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -51,11 +51,10 @@ module ActiveRecord raise NotImplementedError end - def options - reflection.options - end - private + def options + reflection.options + end def associated_records_by_owner(preloader) records = load_records do |record| @@ -113,7 +112,7 @@ module ActiveRecord return {} if owner_keys.empty? # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it - slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) @preloaded_records = slices.flat_map do |slice| records_for(slice).load(&block) end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb index 38e231826c..c20145770f 100644 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -3,7 +3,7 @@ module ActiveRecord class Preloader class BelongsTo < SingularAssociation #:nodoc: def association_key_name - reflection.options[:primary_key] || klass && klass.primary_key + options[:primary_key] || klass && klass.primary_key end def owner_key_name diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index be9dfe7686..8b954138cd 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -16,15 +16,13 @@ module ActiveRecord through_scope) through_records = owners.map do |owner| - association = owner.association through_reflection.name - - center = target_records_from_association(association) + center = owner.association(through_reflection.name).target [owner, Array(center)] end reset_association owners, through_reflection.name - middle_records = through_records.flat_map { |(_,rec)| rec } + middle_records = through_records.flat_map(&:last) preloaders = preloader.preload(middle_records, source_reflection.name, @@ -32,20 +30,18 @@ module ActiveRecord @preloaded_records = preloaders.flat_map(&:preloaded_records) - middle_to_pl = preloaders.each_with_object({}) do |pl,h| + middle_to_pl = preloaders.each_with_object({}) do |pl, h| pl.owners.each { |middle| h[middle] = pl } end - through_records.each_with_object({}) do |(lhs,center), records_by_owner| + through_records.each_with_object({}) do |(lhs, center), records_by_owner| pl_to_middle = center.group_by { |record| middle_to_pl[record] } records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| - association = r.association source_reflection.name - - target_records_from_association(association) + r.association(source_reflection.name).target }.compact # Respect the order on `reflection_scope` if it exists, else use the natural order. @@ -69,7 +65,7 @@ module ActiveRecord def reset_association(owners, association_name) should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) + (options[:source_type] && through_reflection.collection?) # Don't cache the association - we would only be caching a subset if should_reset @@ -98,10 +94,6 @@ module ActiveRecord scope end - - def target_records_from_association(association) - association.loaded? ? association.target : association.reader - end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index e386cc0e4c..91580a28d0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -2,16 +2,8 @@ module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.bar for Foo.has_one :bar - def reader(force_reload = false) - if force_reload && klass - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the parent object instead. - MSG - - klass.uncached { reload } - elsif !loaded? || stale_target? + def reader + if !loaded? || stale_target? reload end @@ -30,6 +22,13 @@ module ActiveRecord record end + # Implements the reload reader method, e.g. foo.reload_bar for + # Foo.has_one :bar + def force_reload_reader + klass.uncached { reload } + target + end + private def create_scope @@ -51,6 +50,8 @@ module ActiveRecord sc.execute(binds, klass, conn) do |record| set_inverse_instance record end.first + rescue ::RangeError + nil end def replace(record) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f4129edc5a..6b87993ba3 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -4,7 +4,7 @@ module ActiveRecord module ThroughAssociation #:nodoc: delegate :source_reflection, :through_reflection, to: :reflection - protected + private # We merge in these scopes for two reasons: # @@ -21,8 +21,6 @@ module ActiveRecord scope end - private - # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 0b08c2a39b..38281158d8 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -128,11 +128,22 @@ module ActiveRecord coder["value"] = value if defined?(@value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :original_attribute alias_method :assigned?, :original_attribute + def original_value_for_database + if assigned? + original_attribute.original_value_for_database + else + _original_value_for_database + end + end + + private def initialize_dup(other) if defined?(@value) && @value.duplicable? @value = @value.dup @@ -143,14 +154,6 @@ module ActiveRecord assigned? && type.changed?(original_value, value, value_before_type_cast) end - def original_value_for_database - if assigned? - original_attribute.original_value_for_database - else - _original_value_for_database - end - end - def _original_value_for_database type.serialize(original_value) end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index a4e2c2ec85..57f8bbed76 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -20,6 +20,8 @@ module ActiveRecord self.class.new(name, user_provided_value, type, original_attribute) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :user_provided_value diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 9843e0ca66..d0dfca0cac 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -12,7 +12,7 @@ module ActiveRecord private - def _assign_attributes(attributes) # :nodoc: + def _assign_attributes(attributes) multi_parameter_attributes = {} nested_parameter_attributes = {} diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 340dfe11cf..5bc8527745 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -3,17 +3,38 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = TypeDecorator.new + class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal: end module ClassMethods # :nodoc: + # This method is an internal API used to create class macros such as + # +serialize+, and features like time zone aware attributes. + # + # Used to wrap the type of an attribute in a new type. + # When the schema for a model is loaded, attributes with the same name as + # +column_name+ will have their type yielded to the given block. The + # return value of that block will be used instead. + # + # Subsequent calls where +column_name+ and +decorator_name+ are the same + # will override the previous decorator, not decorate twice. This can be + # used to create idempotent class macros like +serialize+ def decorate_attribute_type(column_name, decorator_name, &block) matcher = ->(name, _) { name == column_name.to_s } key = "_#{column_name}_#{decorator_name}" decorate_matching_attribute_types(matcher, key, &block) end + # This method is an internal API used to create higher level features like + # time zone aware attributes. + # + # When the schema for a model is loaded, +matcher+ will be called for each + # attribute with its name and type. If the matcher returns a truthy value, + # the type will then be yielded to the given block, and the return value + # of that block will replace the type. + # + # Subsequent calls to this method with the same value for +decorator_name+ + # will replace the previous decorator, not decorate twice. This can be + # used to ensure that class macros are idempotent. def decorate_matching_attribute_types(matcher, decorator_name, &block) reload_schema_from_cache decorator_name = decorator_name.to_s diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 1ed1deec55..83c61fad19 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -62,7 +62,6 @@ module ActiveRecord super(attribute_names) @attribute_methods_generated = true end - true end def undefine_attribute_methods # :nodoc: @@ -394,28 +393,21 @@ module ActiveRecord protected - def clone_attribute_value(reader_method, attribute_name) # :nodoc: - value = send(reader_method, attribute_name) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value + def attribute_method?(attr_name) # :nodoc: + # We check defined? because Syck calls respond_to? before actually calling initialize. + defined?(@attributes) && @attributes.key?(attr_name) end - def arel_attributes_with_values_for_create(attribute_names) # :nodoc: + private + + def arel_attributes_with_values_for_create(attribute_names) arel_attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) # :nodoc: + def arel_attributes_with_values_for_update(attribute_names) arel_attributes_with_values(attributes_for_update(attribute_names)) end - def attribute_method?(attr_name) # :nodoc: - # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.key?(attr_name) - end - - private - # Returns a Hash of the Arel::Attributes and attribute values that have been # typecasted for use in an Arel insert/update method. def arel_attributes_with_values(attribute_names) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index c9638bf70b..76987fb8f4 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "active_support/core_ext/module/attribute_accessors" require "active_record/attribute_mutation_tracker" @@ -13,8 +14,19 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_writes, instance_writer: false - self.partial_writes = true + class_attribute :partial_writes, instance_writer: false, default: true + + after_create { changes_internally_applied } + after_update { changes_internally_applied } + + # Attribute methods for "changed in last call to save?" + attribute_method_affix(prefix: "saved_change_to_", suffix: "?") + attribute_method_prefix("saved_change_to_") + attribute_method_suffix("_before_last_save") + + # Attribute methods for "will change if I call save?" + attribute_method_affix(prefix: "will_save_change_to_", suffix: "?") + attribute_method_suffix("_change_to_be_saved", "_in_database") end # Attempts to +save+ the record and clears changed attributes if successful. @@ -35,9 +47,9 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @mutation_tracker = nil @previous_mutation_tracker = nil - @changed_attributes = HashWithIndifferentAccess.new + clear_mutation_trackers + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end end @@ -46,19 +58,26 @@ module ActiveRecord @attributes = self.class._default_attributes.map do |attr| attr.with_value_from_user(@attributes.fetch_value(attr.name)) end - @mutation_tracker = nil + clear_mutation_trackers + end + + def changes_internally_applied # :nodoc: + @mutations_before_last_save = mutation_tracker + forget_attribute_assignments + @mutations_from_database = AttributeMutationTracker.new(@attributes) end def changes_applied @previous_mutation_tracker = mutation_tracker - @changed_attributes = HashWithIndifferentAccess.new - store_original_attributes + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + clear_mutation_trackers end def clear_changes_information @previous_mutation_tracker = nil - @changed_attributes = HashWithIndifferentAccess.new - store_original_attributes + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + forget_attribute_assignments + clear_mutation_trackers end def raw_write_attribute(attr_name, *) @@ -80,17 +99,27 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else + emit_warning_if_needed("changed_attributes", "saved_changes.transform_values(&:first)") super.reverse_merge(mutation_tracker.changed_values).freeze end end def changes cache_changed_attributes do + emit_warning_if_needed("changes", "saved_changes") super end end def previous_changes + unless previous_mutation_tracker.equal?(mutations_before_last_save) + ActiveSupport::Deprecation.warn(<<-EOW.strip_heredoc) + The behavior of `previous_changes` inside of after callbacks is + deprecated without replacement. In the next release of Rails, + this method inside of `after_save` will return the changes that + were just saved. + EOW + end previous_mutation_tracker.changes end @@ -98,6 +127,114 @@ module ActiveRecord mutation_tracker.changed_in_place?(attr_name) end + # Did this attribute change when we last saved? This method can be invoked + # as `saved_change_to_name?` instead of `saved_change_to_attribute?("name")`. + # Behaves similarly to +attribute_changed?+. This method is useful in + # after callbacks to determine if the call to save changed a certain + # attribute. + # + # ==== Options + # + # +from+ When passed, this method will return false unless the original + # value is equal to the given option + # + # +to+ When passed, this method will return false unless the value was + # changed to the given value + def saved_change_to_attribute?(attr_name, **options) + mutations_before_last_save.changed?(attr_name, **options) + end + + # Returns the change to an attribute during the last save. If the + # attribute was changed, the result will be an array containing the + # original value and the saved value. + # + # Behaves similarly to +attribute_change+. This method is useful in after + # callbacks, to see the change in an attribute that just occurred + # + # This method can be invoked as `saved_change_to_name` in instead of + # `saved_change_to_attribute("name")` + def saved_change_to_attribute(attr_name) + mutations_before_last_save.change_to_attribute(attr_name) + end + + # Returns the original value of an attribute before the last save. + # Behaves similarly to +attribute_was+. This method is useful in after + # callbacks to get the original value of an attribute before the save that + # just occurred + def attribute_before_last_save(attr_name) + mutations_before_last_save.original_value(attr_name) + end + + # Did the last call to `save` have any changes to change? + def saved_changes? + mutations_before_last_save.any_changes? + end + + # Returns a hash containing all the changes that were just saved. + def saved_changes + mutations_before_last_save.changes + end + + # Alias for `attribute_changed?` + def will_save_change_to_attribute?(attr_name, **options) + mutations_from_database.changed?(attr_name, **options) + end + + # Alias for `attribute_change` + def attribute_change_to_be_saved(attr_name) + mutations_from_database.change_to_attribute(attr_name) + end + + # Alias for `attribute_was` + def attribute_in_database(attr_name) + mutations_from_database.original_value(attr_name) + end + + # Alias for `changed?` + def has_changes_to_save? + mutations_from_database.any_changes? + end + + # Alias for `changes` + def changes_to_save + mutations_from_database.changes + end + + # Alias for `changed` + def changed_attribute_names_to_save + changes_to_save.keys + end + + # Alias for `changed_attributes` + def attributes_in_database + changes_to_save.transform_values(&:first) + end + + def attribute_was(*) + emit_warning_if_needed("attribute_was", "attribute_before_last_save") + super + end + + def attribute_change(*) + emit_warning_if_needed("attribute_change", "saved_change_to_attribute") + super + end + + def attribute_changed?(*) + emit_warning_if_needed("attribute_changed?", "saved_change_to_attribute?") + super + end + + def changed?(*) + emit_warning_if_needed("changed?", "saved_changes?") + super + end + + def changed(*) + emit_warning_if_needed("changed", "saved_changes.keys") + super + end + private def mutation_tracker @@ -107,12 +244,47 @@ module ActiveRecord @mutation_tracker ||= AttributeMutationTracker.new(@attributes) end + def emit_warning_if_needed(method_name, new_method_name) + unless mutation_tracker.equal?(mutations_from_database) + ActiveSupport::Deprecation.warn(<<-EOW.squish) + The behavior of `#{method_name}` inside of after callbacks will + be changing in the next version of Rails. The new return value will reflect the + behavior of calling the method after `save` returned (e.g. the opposite of what + it returns now). To maintain the current behavior, use `#{new_method_name}` + instead. + EOW + end + end + + def mutations_from_database + unless defined?(@mutations_from_database) + @mutations_from_database = nil + end + @mutations_from_database ||= mutation_tracker + end + def changes_include?(attr_name) super || mutation_tracker.changed?(attr_name) end def clear_attribute_change(attr_name) mutation_tracker.forget_change(attr_name) + mutations_from_database.forget_change(attr_name) + end + + def attribute_will_change!(attr_name) + super + if self.class.has_attribute?(attr_name) + mutations_from_database.force_change(attr_name) + else + ActiveSupport::Deprecation.warn(<<-EOW.squish) + #{attr_name} is not an attribute known to Active Record. + This behavior is deprecated and will be removed in the next + version of Rails. If you'd like #{attr_name} to be managed + by Active Record, add `attribute :#{attr_name}` to your class. + EOW + mutations_from_database.deprecated_force_change(attr_name) + end end def _update_record(*) @@ -124,18 +296,27 @@ module ActiveRecord end def keys_for_partial_write - changed & self.class.column_names + changed_attribute_names_to_save & self.class.column_names end - def store_original_attributes + def forget_attribute_assignments @attributes = @attributes.map(&:forgetting_assignment) + end + + def clear_mutation_trackers @mutation_tracker = nil + @mutations_from_database = nil + @mutations_before_last_save = nil end def previous_mutation_tracker @previous_mutation_tracker ||= NullMutationTracker.instance end + def mutations_before_last_save + @mutations_before_last_save ||= previous_mutation_tracker + end + def cache_changed_attributes @cached_changed_attributes = changed_attributes yield diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 6243398a52..2f32caa257 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -9,7 +9,7 @@ module ActiveRecord # available. def to_key sync_with_transaction_state - key = self.id + key = id [key] if key end @@ -45,7 +45,12 @@ module ActiveRecord attribute_was(self.class.primary_key) end - protected + def id_in_database + sync_with_transaction_state + attribute_in_database(self.class.primary_key) + end + + private def attribute_method?(attr_name) attr_name == "id" || super @@ -60,7 +65,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 30f7750884..fdc4bf6621 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -4,7 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - protected + private # We want to generate the methods via module_eval rather than # define_method, because define_method is slower on dispatch. @@ -48,8 +48,13 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = attr_name.to_s - name = self.class.primary_key if name == "id".freeze + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + name = self.class.primary_key if name == "id".freeze && self.class.primary_key _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index c70178cd2c..4d9aff76cc 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -26,7 +26,7 @@ module ActiveRecord # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+ # or a class name that the object type should be equal to. # # ==== Example @@ -54,7 +54,7 @@ module ActiveRecord elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else - Coders::YAMLColumn.new(class_name_or_coder) + Coders::YAMLColumn.new(attr_name, class_name_or_coder) end decorate_attribute_type(attr_name, :serialize) do |type| diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index bea1514cdf..4a8d231503 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/strip" - module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -39,7 +37,7 @@ module ActiveRecord end def set_time_zone_without_conversion(value) - ::Time.zone.local_to_utc(value).in_time_zone if value + ::Time.zone.local_to_utc(value).try(:in_time_zone) if value end def map_avoiding_infinite_recursion(value) @@ -59,17 +57,15 @@ module ActiveRecord mattr_accessor :time_zone_aware_attributes, instance_writer: false self.time_zone_aware_attributes = false - class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false - self.skip_time_zone_conversion_for_attributes = [] - - class_attribute :time_zone_aware_types, instance_writer: false - self.time_zone_aware_types = [:datetime, :not_explicitly_configured] + class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: [] + class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ] end module ClassMethods private def inherited(subclass) + super # We need to apply this decorator here, rather than on module inclusion. The closure # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or @@ -80,35 +76,13 @@ module ActiveRecord TimeZoneConverter.new(type) end end - super end def create_time_zone_conversion_attribute?(name, cast_type) enabled_for_column = time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) - result = enabled_for_column && - time_zone_aware_types.include?(cast_type.type) - - if enabled_for_column && - !result && - cast_type.type == :time && - time_zone_aware_types.include?(:not_explicitly_configured) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - Time columns will become time zone aware in Rails 5.1. This - still causes `String`s to be parsed as if they were in `Time.zone`, - and `Time`s to be converted to `Time.zone`. - - To keep the old behavior, you must add the following to your initializer: - - config.active_record.time_zone_aware_types = [:datetime] - - To silence this deprecation warning, add the following: - - config.active_record.time_zone_aware_types = [:datetime, :time] - MESSAGE - end - result + enabled_for_column && time_zone_aware_types.include?(cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index f65c297e01..fe0e01db28 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -8,7 +8,7 @@ module ActiveRecord end module ClassMethods - protected + private def define_method_attribute=(name) safe_name = name.unpack("h*".freeze).first @@ -29,7 +29,13 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - write_attribute_with_type_cast(attr_name, value, true) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + write_attribute_with_type_cast(name, value, true) end def raw_write_attribute(attr_name, value) # :nodoc: diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index c257aef52f..4de993e169 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -1,7 +1,11 @@ module ActiveRecord class AttributeMutationTracker # :nodoc: + OPTION_NOT_GIVEN = Object.new + def initialize(attributes) @attributes = attributes + @forced_changes = Set.new + @deprecated_forced_changes = Set.new end def changed_values @@ -14,15 +18,29 @@ module ActiveRecord def changes attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| - if changed?(attr_name) - result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + change = change_to_attribute(attr_name) + if change + result[attr_name] = change end end end - def changed?(attr_name) + def change_to_attribute(attr_name) + if changed?(attr_name) + [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + end + end + + def any_changes? + attr_names.any? { |attr| changed?(attr) } || deprecated_forced_changes.any? + end + + def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) attr_name = attr_name.to_s - attributes[attr_name].changed? + forced_changes.include?(attr_name) || + attributes[attr_name].changed? && + (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) && + (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to) end def changed_in_place?(attr_name) @@ -32,11 +50,26 @@ module ActiveRecord def forget_change(attr_name) attr_name = attr_name.to_s attributes[attr_name] = attributes[attr_name].forgetting_assignment + forced_changes.delete(attr_name) + end + + def original_value(attr_name) + attributes[attr_name].original_value + end + + def force_change(attr_name) + forced_changes << attr_name.to_s end + def deprecated_force_change(attr_name) + deprecated_forced_changes << attr_name.to_s + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :attributes + attr_reader :attributes, :forced_changes, :deprecated_forced_changes private @@ -48,14 +81,21 @@ module ActiveRecord class NullMutationTracker # :nodoc: include Singleton - def changed_values + def changed_values(*) {} end - def changes + def changes(*) {} end + def change_to_attribute(attr_name) + end + + def any_changes?(*) + false + end + def changed?(*) false end @@ -66,5 +106,8 @@ module ActiveRecord def forget_change(*) end + + def original_value(*) + end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 5bde1f107c..01f9d815d5 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -64,7 +64,7 @@ module ActiveRecord end def deep_dup - dup.tap do |copy| + self.class.allocate.tap do |copy| copy.instance_variable_set(:@attributes, attributes.deep_dup) end end @@ -98,6 +98,8 @@ module ActiveRecord attributes == other.attributes end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 661f996e1a..2f624d32af 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -90,6 +90,8 @@ module ActiveRecord @materialized = true end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :types, :values, :additional_types, :delegate_hash, :default diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb index c86cfc4263..899de14792 100644 --- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb +++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb @@ -31,6 +31,8 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :default_types diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 4b92e5835f..475b9beec4 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -6,8 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: - self.attributes_to_define_after_schema_loads = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal: end module ClassMethods @@ -116,7 +115,7 @@ module ActiveRecord # Users may also define their own custom types, as long as they respond # to the methods defined on the value type. The method +deserialize+ or # +cast+ will be called on your type object, with raw input from the - # database or from your controllers. See ActiveRecord::Type::Value for the + # database or from your controllers. See ActiveModel::Type::Value for the # expected API. It is recommended that your type objects inherit from an # existing type, or from ActiveRecord::Type::Value # @@ -143,7 +142,7 @@ module ActiveRecord # store_listing.price_in_cents # => 1000 # # For more details on creating custom types, see the documentation for - # ActiveRecord::Type::Value. For more details on registering your types + # ActiveModel::Type::Value. For more details on registering your types # to be referenced by a symbol, see ActiveRecord::Type.register. You can # also pass a type object directly, in place of a symbol. # @@ -190,8 +189,8 @@ module ActiveRecord # The type of an attribute is given the opportunity to change how dirty # tracking is performed. The methods +changed?+ and +changed_in_place?+ # will be called from ActiveModel::Dirty. See the documentation for those - # methods in ActiveRecord::Type::Value for more details. - def attribute(name, cast_type, **options) + # methods in ActiveModel::Type::Value for more details. + def attribute(name, cast_type = Type::Value.new, **options) name = name.to_s reload_schema_from_cache diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index d3e0dee731..829a4f6e86 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -154,10 +154,10 @@ module ActiveRecord # Loop prevention for validation of associations unless @_already_called[name] begin - @_already_called[name]=true + @_already_called[name] = true result = instance_eval(&block) ensure - @_already_called[name]=false + @_already_called[name] = false end end @@ -181,6 +181,7 @@ module ActiveRecord if reflection.collection? before_save :before_save_collection_association + after_save :after_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } # Doesn't use after_save as that would save associations added in after_create/after_update twice @@ -215,13 +216,7 @@ module ActiveRecord method = :validate_single_association end - define_non_cyclic_method(validation_method) do - send(method, reflection) - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true - end + define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end @@ -267,7 +262,7 @@ module ActiveRecord # Returns whether or not this record has been changed in any way (including whether # any of its nested autosave associations are likewise changed) def changed_for_autosave? - new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? + new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave? end private @@ -325,12 +320,12 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record, index=nil) + def association_valid?(reflection, record, index = nil) return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) - validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + context = validation_context unless [:create, :update].include?(validation_context) - unless valid = record.valid?(validation_context) + unless valid = record.valid?(context) if reflection.options[:autosave] indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) @@ -368,7 +363,10 @@ module ActiveRecord # association whether or not the parent was a new record before saving. def before_save_collection_association @new_record_before_save = new_record? - true + end + + def after_save_collection_association + @new_record_before_save = false end # Saves any new associated records, or all loaded autosave associations if @@ -383,6 +381,9 @@ module ActiveRecord if association = association_instance_get(reflection.name) autosave = reflection.options[:autosave] + # reconstruct the scope now that we know the owner's id + association.reset_scope + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave records_to_destroy = records.select(&:marked_for_destruction?) @@ -408,9 +409,6 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end end - - # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) end end @@ -451,7 +449,7 @@ module ActiveRecord def record_changed?(reflection, record, key) record.new_record? || (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || - record.attribute_changed?(reflection.foreign_key) + record.will_save_change_to_attribute?(reflection.foreign_key) end # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1e7e939097..ac1aa2df45 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -14,6 +14,7 @@ require "active_support/core_ext/module/introspection" require "active_support/core_ext/object/duplicable" require "active_support/core_ext/class/subclasses" require "active_record/attribute_decorators" +require "active_record/define_callbacks" require "active_record/errors" require "active_record/log_subscriber" require "active_record/explain_subscriber" @@ -303,6 +304,7 @@ module ActiveRecord #:nodoc: include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic + include DefineCallbacks include AttributeMethods include Callbacks include Timestamp diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index c616733aa4..eb44887e18 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -225,6 +225,55 @@ module ActiveRecord # # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # + # Also, there are cases when you want several callbacks of the same type to + # be executed in order. + # + # For example: + # + # class Topic + # has_many :children + # + # after_save :log_children + # after_save :do_something_else + # + # private + # + # def log_chidren + # # Child processing + # end + # + # def do_something_else + # # Something else + # end + # end + # + # In this case the +log_children+ gets executed before +do_something_else+. + # The same applies to all non-transactional callbacks. + # + # In case there are multiple transactional callbacks as seen below, the order + # is reversed. + # + # For example: + # + # class Topic + # has_many :children + # + # after_commit :log_children + # after_commit :do_something_else + # + # private + # + # def log_chidren + # # Child processing + # end + # + # def do_something_else + # # Something else + # end + # end + # + # In this case the +do_something_else+ gets executed before +log_children+. + # # == \Transactions # # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!], @@ -265,17 +314,6 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] - module ClassMethods # :nodoc: - include ActiveModel::Callbacks - end - - included do - include ActiveModel::Validations::Callbacks - - define_model_callbacks :initialize, :find, :touch, only: :after - define_model_callbacks :save, :create, :update, :destroy - end - def destroy #:nodoc: @_destroy_callback_already_called ||= false return if @_destroy_callback_already_called @@ -294,15 +332,15 @@ module ActiveRecord private - def create_or_update(*) #:nodoc: + def create_or_update(*) _run_save_callbacks { super } end - def _create_record #:nodoc: + def _create_record _run_create_callbacks { super } end - def _update_record(*) #:nodoc: + def _update_record(*) _run_update_callbacks { super } end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 1c8c9fa272..9c52a31b95 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,12 +1,12 @@ require "yaml" -require "active_support/core_ext/regexp" module ActiveRecord module Coders # :nodoc: class YAMLColumn # :nodoc: attr_accessor :object_class - def initialize(object_class = Object) + def initialize(attr_name, object_class = Object) + @attr_name = attr_name @object_class = object_class check_arity_of_constructor end @@ -14,7 +14,7 @@ module ActiveRecord def dump(obj) return if obj.nil? - assert_valid_value(obj) + assert_valid_value(obj, action: "dump") YAML.dump obj end @@ -23,27 +23,25 @@ module ActiveRecord return yaml unless yaml.is_a?(String) && /^---/.match?(yaml) obj = YAML.load(yaml) - assert_valid_value(obj) + assert_valid_value(obj, action: "load") obj ||= object_class.new if object_class != Object obj end - def assert_valid_value(obj) + def assert_valid_value(obj, action:) unless obj.nil? || obj.is_a?(object_class) raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" end end private def check_arity_of_constructor - begin - load(nil) - rescue ArgumentError - raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." - end + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." end end end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 43784b70e3..8b937b6703 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -7,17 +7,27 @@ module ActiveRecord if collection.loaded? size = collection.size if size > 0 - timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + timestamp = collection.max_by(×tamp_column)._read_attribute(timestamp_column) end else column_type = type_for_attribute(timestamp_column.to_s) column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" - query = collection - .unscope(:select) - .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") - .unscope(:order) - result = connection.select_one(query) + if collection.limit_value || collection.offset_value + query = collection.spawn + query.select_values = [column] + subquery_alias = "subquery_for_cache_key" + subquery_column = "#{subquery_alias}.#{timestamp_column}" + subquery = query.arel.as(subquery_alias) + arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) + else + query = collection.unscope(:order) + query.select_values = [select_values % column] + arel = query.arel + end + + result = connection.select_one(arel, nil, query.bound_attributes) if result.blank? size = 0 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 2d62fd8d50..61bf5477aa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -69,7 +69,7 @@ module ActiveRecord # 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). + # blocking wait. (Default +nil+, which means don't schedule the Reaper). # #-- # Synchronization policy: @@ -116,7 +116,7 @@ module ActiveRecord end end - # If +element+ is in the queue, remove and return it, or nil. + # If +element+ is in the queue, remove and return it, or +nil+. def delete(element) synchronize do @queue.delete(element) @@ -135,7 +135,7 @@ module ActiveRecord # If +timeout+ is not given, remove and return the head the # queue if the number of available elements is strictly # greater than the number of threads currently waiting (that - # is, don't jump ahead in line). Otherwise, return nil. + # is, don't jump ahead in line). Otherwise, return +nil+. # # If +timeout+ is given, block if there is no element # available, waiting up to +timeout+ seconds for an element to @@ -171,14 +171,14 @@ module ActiveRecord @queue.size > @num_waiting end - # Removes and returns the head of the queue if possible, or nil. + # Removes and returns the head of the queue if possible, or +nil+. def remove @queue.shift end # Remove and return the head the queue if the number of # available elements is strictly greater than the number of - # threads currently waiting. Otherwise, return nil. + # threads currently waiting. Otherwise, return +nil+. def no_wait_poll remove if can_remove_no_wait? end @@ -282,7 +282,7 @@ module ActiveRecord end # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. - # A reaper instantiated with a nil frequency will never reap the + # A reaper instantiated with a +nil+ frequency will never reap the # connection pool. # # Configure the frequency by setting "reaping_frequency" in your @@ -307,6 +307,7 @@ module ActiveRecord end include MonitorMixin + include QueryCache::ConnectionPoolConfiguration attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache attr_reader :spec, :connections, :size, :reaper @@ -349,10 +350,19 @@ module ActiveRecord # currently in the process of independently establishing connections to the DB. @now_connecting = 0 - # A boolean toggle that allows/disallows new connections. - @new_cons_enabled = true + @threads_blocking_new_connections = 0 @available = ConnectionLeasingQueue.new self + + @lock_thread = false + end + + def lock_thread=(lock_thread) + if lock_thread + @lock_thread = Thread.current + else + @lock_thread = nil + end end # Retrieve the connection associated with the current thread, or call @@ -361,7 +371,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a cache keyed by a thread. def connection - @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout + @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout end # Returns true if there is an open connection being used for the current thread. @@ -445,8 +455,6 @@ module ActiveRecord # connections in the pool within a timeout interval (default duration is # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def clear_reloadable_connections(raise_on_acquisition_timeout = true) - num_new_conns_required = 0 - with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @connections.each do |conn| @@ -457,24 +465,9 @@ module ActiveRecord conn.disconnect! if conn.requires_reloading? end @connections.delete_if(&:requires_reloading?) - @available.clear - - if @connections.size < @size - # because of the pruning done by this method, we might be running - # low on connections, while threads stuck in queue are helpless - # (not being able to establish new connections for themselves), - # see also more detailed explanation in +remove+ - num_new_conns_required = num_waiting_in_queue - @connections.size - end - - @connections.each do |conn| - @available.add conn - end end end - - bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 end # Clears the cache which maps classes and re-connects connections that @@ -513,14 +506,16 @@ module ActiveRecord # +conn+: an AbstractAdapter object, which was obtained by earlier by # calling #checkout on this pool. def checkin(conn) - synchronize do - remove_connection_from_thread_cache conn + conn.lock.synchronize do + synchronize do + remove_connection_from_thread_cache conn - conn._run_checkin_callbacks do - conn.expire - end + conn._run_checkin_callbacks do + conn.expire + end - @available.add conn + @available.add conn + end end end @@ -581,6 +576,24 @@ module ActiveRecord @available.num_waiting end + # Return connection pool's usage statistic + # Example: + # + # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 } + def stat + synchronize do + { + size: size, + connections: @connections.size, + busy: @connections.count { |c| c.in_use? && c.owner.alive? }, + dead: @connections.count { |c| c.in_use? && !c.owner.alive? }, + idle: @connections.count { |c| !c.in_use? }, + waiting: num_waiting_in_queue, + checkout_timeout: checkout_timeout + } + end + end + private #-- # this is unfortunately not concurrent @@ -681,13 +694,32 @@ module ActiveRecord end def with_new_connections_blocked - previous_value = nil synchronize do - previous_value, @new_cons_enabled = @new_cons_enabled, false + @threads_blocking_new_connections += 1 end + yield ensure - synchronize { @new_cons_enabled = previous_value } + num_new_conns_required = 0 + + synchronize do + @threads_blocking_new_connections -= 1 + + if @threads_blocking_new_connections.zero? + @available.clear + + num_new_conns_required = num_waiting_in_queue + + @connections.each do |conn| + next if conn.in_use? + + @available.add conn + num_new_conns_required -= 1 + end + end + end + + bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 end # Acquire a connection by one of 1) immediately removing one @@ -739,7 +771,7 @@ module ActiveRecord # and increment @now_connecting, to prevent overstepping this pool's @size # constraint do_checkout = synchronize do - if @new_cons_enabled && (@connections.size + @now_connecting) < @size + if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size @now_connecting += 1 end end @@ -795,7 +827,7 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # establish_connection "library_db" + # establish_connection :library_db # end # # class ScaryBook < Book @@ -827,13 +859,13 @@ module ActiveRecord # All Active Record models use this handler to determine the connection pool that they # should use. # - # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge + # The ConnectionHandler class is not coupled with the Active models, as it has no knowledge # about the model. The model needs to pass a specification name to the handler, - # in order to lookup the correct connection pool. + # in order to look up the correct connection pool. class ConnectionHandler def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). - @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h,k| + @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| h[k] = Concurrent::Map.new(initial_capacity: 2) end end @@ -947,7 +979,7 @@ module ActiveRecord end def pool_from_any_process_for(spec_name) - owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } + owner_to_pool = @owner_to_pool.values.reverse.find { |v| v[spec_name] } owner_to_pool && owner_to_pool[spec_name] end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 95c72f1e20..407e019326 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -46,7 +46,7 @@ module ActiveRecord end # Returns the maximum number of elements in an IN (x,y,z) clause. - # nil means no limit. + # +nil+ means no limit. def in_clause_length nil end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index aa2dfdd573..c6811a4802 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -10,9 +10,9 @@ module ActiveRecord def to_sql(arel, binds = []) if arel.respond_to?(:ast) collected = visitor.accept(arel.ast, collector) - collected.compile(binds, self) + collected.compile(binds, self).freeze else - arel + arel.dup.freeze end end @@ -51,8 +51,7 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - if result = select_rows(to_sql(arel, binds), name, binds).first + if result = select_rows(arel, name, binds).first result.first end end @@ -60,14 +59,13 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - select_rows(to_sql(arel, binds), name, binds).map(&:first) + select_rows(arel, name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + def select_rows(arel, name = nil, binds = []) + select_all(arel, name, binds).rows end # Executes the SQL statement in the context of this connection and returns @@ -115,7 +113,7 @@ module ActiveRecord # Executes an INSERT query and returns the new record's ID # - # +id_value+ will be returned unless the value is nil, in + # +id_value+ will be returned unless the value is +nil+, in # which case the database will attempt to calculate the last inserted # id and return that value. # @@ -126,28 +124,23 @@ module ActiveRecord id_value || last_inserted_id(value) end alias create insert - alias insert_sql insert - deprecate insert_sql: :insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) exec_update(to_sql(arel, binds), name, binds) end - alias update_sql update - deprecate update_sql: :update # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) exec_delete(to_sql(arel, binds), name, binds) end - alias delete_sql delete - deprecate delete_sql: :delete # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ - def supports_statement_cache? - false + def supports_statement_cache? # :nodoc: + true end + deprecate :supports_statement_cache? # Runs the given block in a database transaction, and returns the result # of the block. @@ -334,17 +327,12 @@ module ActiveRecord # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers, or - # an Arel SQL literal. + # should look like an integer, or an Arel SQL literal. # # Returns Integer and Arel::Nodes::SqlLiteral limits as is. - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. def sanitize_limit(limit) if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) limit - elsif limit.to_s.include?(",") - Arel.sql limit.to_s.split(",").map { |i| Integer(i) }.join(",") else Integer(limit) end @@ -360,7 +348,7 @@ module ActiveRecord end alias join_to_delete join_to_update - protected + private # 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/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 6ca53c72ce..e53ba4e666 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -4,6 +4,9 @@ module ActiveRecord class << self def included(base) #:nodoc: dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction + + base.set_callback :checkout, :after, :configure_query_cache! + base.set_callback :checkin, :after, :disable_query_cache! end def dirties_query_cache(base, *method_names) @@ -18,11 +21,32 @@ module ActiveRecord end end + module ConnectionPoolConfiguration + def initialize(*) + super + @query_cache_enabled = Concurrent::Map.new { false } + end + + def enable_query_cache! + @query_cache_enabled[connection_cache_key(Thread.current)] = true + connection.enable_query_cache! if active_connection? + end + + def disable_query_cache! + @query_cache_enabled.delete connection_cache_key(Thread.current) + connection.disable_query_cache! if active_connection? + end + + def query_cache_enabled + @query_cache_enabled[connection_cache_key(Thread.current)] + end + end + attr_reader :query_cache, :query_cache_enabled def initialize(*) super - @query_cache = Hash.new { |h,sql| h[sql] = {} } + @query_cache = Hash.new { |h, sql| h[sql] = {} } @query_cache_enabled = false end @@ -41,6 +65,7 @@ module ActiveRecord def disable_query_cache! @query_cache_enabled = false + clear_query_cache end # Disable the query cache within the block. @@ -58,14 +83,16 @@ module ActiveRecord # the same SQL query and repeatedly return the same result each time, silently # undermining the randomness you were expecting. def clear_query_cache - @query_cache.clear + @lock.synchronize do + @query_cache.clear + end end def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) } + cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) } else super end @@ -73,16 +100,24 @@ module ActiveRecord private - def cache_sql(sql, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - sql: sql, binds: binds, name: "CACHE", connection_id: object_id) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end - result.dup + def cache_sql(sql, name, binds) + @lock.synchronize do + result = + if @query_cache[sql].key?(binds) + ActiveSupport::Notifications.instrument( + "sql.active_record", + sql: sql, + binds: binds, + name: name, + connection_id: object_id, + cached: true, + ) + @query_cache[sql][binds] + else + @query_cache[sql][binds] = yield + end + result.dup + end end # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such @@ -90,6 +125,10 @@ module ActiveRecord def locked?(arel) arel.respond_to?(:locked) && arel.locked end + + def configure_query_cache! + enable_query_cache! if pool.query_cache_enabled + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index bbd52b8a91..61233dcc51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -1,22 +1,25 @@ require "active_support/core_ext/big_decimal/conversions" +require "active_support/multibyte/chars" module ActiveRecord module ConnectionAdapters # :nodoc: module Quoting # Quotes the column value to help prevent # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. - def quote(value, column = nil) - # records are quoted as their primary key - return value.quoted_id if value.respond_to?(:quoted_id) + def quote(value) + value = id_value_for_database(value) if value.is_a?(Base) - if column - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a column to `quote` has been deprecated. It is only used - for type casting, which should be handled elsewhere. See - https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 - for more information. - MSG - value = type_cast_from_column(column, value) + if value.respond_to?(:quoted_id) + at = value.method(:quoted_id).source_location + at &&= " at %s:%d" % at + + owner = value.method(:quoted_id).owner.to_s + klass = value.class.to_s + klass += "(#{owner})" unless owner == klass + + ActiveSupport::Deprecation.warn \ + "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})" + return value.quoted_id end _quote(value) @@ -26,6 +29,8 @@ module ActiveRecord # SQLite does not understand dates, so this method will convert a Date # to a String. def type_cast(value, column = nil) + value = id_value_for_database(value) if value.is_a?(Base) + if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end @@ -63,17 +68,6 @@ module ActiveRecord lookup_cast_type(column.sql_type) end - def fetch_type_metadata(sql_type) - cast_type = lookup_cast_type(sql_type) - SqlTypeMetadata.new( - sql_type: sql_type, - type: cast_type.type, - limit: cast_type.limit, - precision: cast_type.precision, - scale: cast_type.scale, - ) - end - # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) @@ -150,11 +144,28 @@ module ActiveRecord quoted_date(value).sub(/\A2000-01-01 /, "") end - private + def quoted_binary(value) # :nodoc: + "'#{quote_string(value.to_s)}'" + end - def type_casted_binds(binds) + def type_casted_binds(binds) # :nodoc: + if binds.first.is_a?(Array) + binds.map { |column, value| type_cast(value, column) } + else binds.map { |attr| type_cast(attr.value_for_database) } end + end + + private + def lookup_cast_type(sql_type) + type_map.lookup(sql_type) + end + + def id_value_for_database(value) + if primary_key = value.class.primary_key + value.instance_variable_get(:@attributes)[primary_key].value_for_database + end + end def types_which_need_no_typecasting [nil, Numeric, String] @@ -162,7 +173,7 @@ module ActiveRecord def _quote(value) case value - when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + when String, ActiveSupport::Multibyte::Chars "'#{quote_string(value.to_s)}'" when true then quoted_true when false then quoted_false @@ -170,6 +181,7 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s("F") when Numeric, ActiveSupport::Duration then value.to_s + when Type::Binary::Data then quoted_binary(value) when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" 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 322684672f..a4fecc4a8e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,9 +15,9 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options private @@ -29,7 +29,7 @@ module ActiveRecord end def visit_ColumnDefinition(o) - o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type = type_to_sql(o.type, o.options) column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql @@ -49,7 +49,7 @@ module ActiveRecord statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) end - if supports_foreign_keys? + if supports_foreign_keys_in_create? statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end @@ -96,17 +96,7 @@ module ActiveRecord end def column_options(o) - column_options = {} - column_options[:null] = o.null unless o.null.nil? - column_options[:default] = o.default unless o.default.nil? - column_options[:column] = o - column_options[:first] = o.first - column_options[:after] = o.after - column_options[:auto_increment] = o.auto_increment - column_options[:primary_key] = o.primary_key - column_options[:collation] = o.collation - column_options[:comment] = o.comment - column_options + o.options.merge(column: o) end def add_column_options!(sql, options) @@ -143,5 +133,6 @@ module ActiveRecord end end end + SchemaCreation = AbstractAdapter::SchemaCreation # :nodoc: end end 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 ffde4f2c93..a30fbe0e05 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -2,30 +2,63 @@ module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database - # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: + # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes + class IndexDefinition # :nodoc: + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + + def initialize( + table, name, + unique = false, + columns = [], + lengths: {}, + orders: {}, + where: nil, + type: nil, + using: nil, + comment: nil + ) + @table = table + @name = name + @unique = unique + @columns = columns + @lengths = lengths + @orders = orders + @where = where + @type = type + @using = using + @comment = comment + end end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc: + ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc: def primary_key? - primary_key || type.to_sym == :primary_key + options[:primary_key] end - end - class AddColumnDefinition < Struct.new(:column) # :nodoc: - end + [:limit, :precision, :scale, :default, :null, :collation, :comment].each do |option_name| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{option_name} + options[:#{option_name}] + end - class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: + def #{option_name}=(value) + options[:#{option_name}] = value + end + CODE + end end - class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: - end + AddColumnDefinition = Struct.new(:column) # :nodoc: - class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + ChangeColumnDefinition = Struct.new(:column, :name) #:nodoc: + + PrimaryKeyDefinition = Struct.new(:name) # :nodoc: + + ForeignKeyDefinition = Struct.new(:from_table, :to_table, :options) do #:nodoc: def name options[:name] end @@ -71,7 +104,7 @@ module ActiveRecord polymorphic: false, index: true, foreign_key: false, - type: :integer, + type: :bigint, **options ) @name = name @@ -100,22 +133,20 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options private - def as_options(value, default = {}) - if value.is_a?(Hash) - value - else - default - end + def as_options(value) + value.is_a?(Hash) ? value : {} end def polymorphic_options - as_options(polymorphic, options) + as_options(polymorphic).merge(null: options[:null]) end def index_options @@ -175,6 +206,7 @@ module ActiveRecord :text, :time, :timestamp, + :virtual, ].each do |column_type| module_eval <<-CODE, __FILE__, __LINE__ + 1 def #{column_type}(*args, **options) @@ -355,33 +387,22 @@ module ActiveRecord # # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) - args.each do |col| - ReferenceDefinition.new(col, **options).add_to(self) + args.each do |ref_name| + ReferenceDefinition.new(ref_name, options).add_to(self) end end alias :belongs_to :references - def new_column_definition(name, type, options) # :nodoc: + def new_column_definition(name, type, **options) # :nodoc: type = aliased_types(type.to_s, type) - column = create_column_definition name, type - - column.limit = options[:limit] - column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - column.first = options[:first] - column.after = options[:after] - column.auto_increment = options[:auto_increment] - column.primary_key = type == :primary_key || options[:primary_key] - column.collation = options[:collation] - column.comment = options[:comment] - column + options[:primary_key] ||= type == :primary_key + options[:null] = false if options[:primary_key] + create_column_definition(name, type, options) end private - def create_column_definition(name, type) - ColumnDefinition.new name, type + def create_column_definition(name, type, options) + ColumnDefinition.new(name, type, options) end def aliased_types(name, fallback) @@ -475,7 +496,7 @@ module ActiveRecord # Checks to see if a column exists. # - # t.string(:name) unless t.column_exists?(:name, :string) + # t.string(:name) unless t.column_exists?(:name, :string) # # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?] def column_exists?(column_name, type = nil, options = {}) @@ -496,9 +517,9 @@ module ActiveRecord # Checks to see if an index exists. # - # unless t.index_exists?(:branch_id) - # t.index(:branch_id) - # end + # unless t.index_exists?(:branch_id) + # t.index(:branch_id) + # end # # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] def index_exists?(column_name, options = {}) @@ -589,8 +610,7 @@ module ActiveRecord # t.belongs_to(:supplier, foreign_key: true) # # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. - def references(*args) - options = args.extract_options! + def references(*args, **options) args.each do |ref_name| @base.add_reference(name, ref_name, options) end @@ -603,8 +623,7 @@ module ActiveRecord # t.remove_belongs_to(:supplier, polymorphic: true) # # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] - def remove_references(*args) - options = args.extract_options! + def remove_references(*args, **options) args.each do |ref_name| @base.remove_reference(name, ref_name, options) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 8bb7362c2e..34036d8a1d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -7,16 +7,15 @@ module ActiveRecord # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column) - spec = Hash[prepare_column_options(column).map { |k, v| [k, "#{k}: #{v}"] }] - spec[:name] = column.name.inspect - spec[:type] = schema_type(column).to_s - spec + [schema_type_with_virtual(column), prepare_column_options(column)] end def column_spec_for_primary_key(column) return {} if default_primary_key?(column) spec = { id: schema_type(column).inspect } spec.merge!(prepare_column_options(column).except!(:null)) + spec[:default] ||= "nil" if explicit_primary_key_default?(column) + spec end # This can be overridden on an Adapter level basis to support other @@ -38,7 +37,7 @@ module ActiveRecord end default = schema_default(column) if column.has_default? - spec[:default] = default unless default.nil? + spec[:default] = default unless default.nil? spec[:null] = "false" unless column.null @@ -52,14 +51,27 @@ module ActiveRecord end # Lists the valid migration options - def migration_keys - [:name, :limit, :precision, :scale, :default, :null, :collation, :comment] + def migration_keys # :nodoc: + column_options_keys end + deprecate :migration_keys private def default_primary_key?(column) - schema_type(column) == :integer + schema_type(column) == :bigint + end + + def explicit_primary_key_default?(column) + false + end + + def schema_type_with_virtual(column) + if supports_virtual_columns? && column.virtual? + :virtual + else + schema_type(column) + end end def schema_type(column) 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 29520ed9c8..16a398f631 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -31,6 +31,8 @@ module ActiveRecord # Returns the relation names useable to back Active Record models. # For most adapters this means all #tables and #views. def data_sources + select_values(data_source_sql, "SCHEMA") + rescue NotImplementedError tables | views end @@ -39,12 +41,14 @@ module ActiveRecord # data_source_exists?(:ebooks) # def data_source_exists?(name) + select_values(data_source_sql(name), "SCHEMA").any? if name.present? + rescue NotImplementedError data_sources.include?(name.to_s) end # Returns an array of table names defined in the database. - def tables(name = nil) - raise NotImplementedError, "#tables is not implemented" + def tables + select_values(data_source_sql(type: "BASE TABLE"), "SCHEMA") end # Checks to see if the table +table_name+ exists on the database. @@ -52,12 +56,14 @@ module ActiveRecord # table_exists?(:developers) # def table_exists?(table_name) + select_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present? + rescue NotImplementedError tables.include?(table_name.to_s) end # Returns an array of view names defined in the database. def views - raise NotImplementedError, "#views is not implemented" + select_values(data_source_sql(type: "VIEW"), "SCHEMA") end # Checks to see if the view +view_name+ exists on the database. @@ -65,11 +71,15 @@ module ActiveRecord # view_exists?(:ebooks) # def view_exists?(view_name) + select_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present? + rescue NotImplementedError views.include?(view_name.to_s) end # Returns an array of indexes for the given table. - # def indexes(table_name, name = nil) end + def indexes(table_name, name = nil) + raise NotImplementedError, "#indexes is not implemented" + end # Checks to see if an index exists on a table for a given index definition. # @@ -95,10 +105,12 @@ module ActiveRecord indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end - # Returns an array of Column objects for the table specified by +table_name+. - # See the concrete implementation for details on the expected parameter values. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) - raise NotImplementedError, "#columns is not implemented" + table_name = table_name.to_s + column_definitions(table_name).map do |field| + new_column_from_field(table_name, field) + end end # Checks to see if a column exists in a given table. @@ -120,7 +132,7 @@ module ActiveRecord checks = [] checks << lambda { |c| c.name == column_name } checks << lambda { |c| c.type == type } if type - (migration_keys - [:name]).each do |attr| + column_options_keys.each do |attr| checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) end @@ -271,8 +283,8 @@ module ActiveRecord yield td if block_given? - if options[:force] && data_source_exists?(table_name) - drop_table(table_name, options) + if options[:force] + drop_table(table_name, **options, if_exists: true) end result = execute schema_creation.accept td @@ -284,10 +296,10 @@ module ActiveRecord end if supports_comments? && !supports_comments_in_create? - change_table_comment(table_name, comment) if comment + change_table_comment(table_name, comment) if comment.present? td.columns.each do |column| - change_column_comment(table_name, column.name, column.comment) if column.comment + change_column_comment(table_name, column.name, column.comment) if column.comment.present? end end @@ -332,18 +344,16 @@ module ActiveRecord # part_id int NOT NULL, # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # - def create_join_table(table_1, table_2, options = {}) + def create_join_table(table_1, table_2, column_options: {}, **options) join_table_name = find_join_table_name(table_1, table_2, options) - column_options = options.delete(:column_options) || {} - column_options.reverse_merge!(null: false) - type = column_options.delete(:type) || :integer + column_options.reverse_merge!(null: false, index: false) - t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key } + t1_ref, t2_ref = [table_1, table_2].map { |t| t.to_s.singularize } create_table(join_table_name, options.merge!(id: false)) do |td| - td.send type, t1_column, column_options - td.send type, t2_column, column_options + td.references t1_ref, column_options + td.references t2_ref, column_options yield td if block_given? end end @@ -766,16 +776,17 @@ module ActiveRecord raise ArgumentError, "You must specify the index name" end else - index_name(table_name, column: options) + index_name(table_name, index_name_options(options)) end end # Verifies the existence of an index with a given name. - # - # The default argument is returned if the underlying implementation does not define the indexes method, - # as there's no way to determine the correct answer in that case. - def index_name_exists?(table_name, index_name, default) - return default unless respond_to?(:indexes) + def index_name_exists?(table_name, index_name, default = nil) + unless default.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing default to #index_name_exists? is deprecated without replacement. + MSG + end index_name = index_name.to_s indexes(table_name).detect { |i| i.name == index_name } end @@ -826,8 +837,8 @@ module ActiveRecord # # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) # - def add_reference(table_name, *args) - ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) + def add_reference(table_name, ref_name, **options) + ReferenceDefinition.new(ref_name, options).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -854,6 +865,7 @@ module ActiveRecord else foreign_key_options = { to_table: reference_name } end + foreign_key_options[:column] ||= "#{ref_name}_id" remove_foreign_key(table_name, **foreign_key_options) end @@ -964,16 +976,6 @@ module ActiveRecord foreign_key_for(from_table, options_or_to_table).present? end - def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: - return unless supports_foreign_keys? - foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } - end - - def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: - foreign_key_for(from_table, options_or_to_table) || \ - raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}") - end - def foreign_key_column_for(table_name) # :nodoc: prefix = Base.table_name_prefix suffix = Base.table_name_suffix @@ -989,34 +991,19 @@ module ActiveRecord end def dump_schema_information #:nodoc: - versions = ActiveRecord::SchemaMigration.order("version").pluck(:version) + versions = ActiveRecord::SchemaMigration.all_versions insert_versions_sql(versions) end - def insert_versions_sql(versions) # :nodoc: - sm_table = ActiveRecord::Migrator.schema_migrations_table_name - - if supports_multi_insert? - sql = "INSERT INTO #{sm_table} (version) VALUES\n" - sql << versions.map { |v| "('#{v}')" }.join(",\n") - sql << ";\n\n" - sql - else - versions.map { |version| - "INSERT INTO #{sm_table} (version) VALUES ('#{version}');" - }.join "\n\n" - end - end - - # Should not be called normally, but this operation is non-destructive. - # The migrations module handles this automatically. - def initialize_schema_migrations_table + def initialize_schema_migrations_table # :nodoc: ActiveRecord::SchemaMigration.create_table end + deprecate :initialize_schema_migrations_table - def initialize_internal_metadata_table + def initialize_internal_metadata_table # :nodoc: ActiveRecord::InternalMetadata.create_table end + deprecate :initialize_internal_metadata_table def internal_string_options_for_primary_key # :nodoc: { primary_key: true } @@ -1025,16 +1012,15 @@ module ActiveRecord def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i - sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) + sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - paths = migrations_paths.map { |p| "#{p}/[0-9]*_*.rb" } - versions = Dir[*paths].map do |filename| - filename.split("/").last.split("_").first.to_i + 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 end unless migrated.include?(version) - execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" + execute "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})" end inserting = (versions - migrated).select { |v| v < version } @@ -1042,11 +1028,17 @@ module ActiveRecord if (duplicate = inserting.detect { |v| inserting.count(v) > 1 }) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - execute insert_versions_sql(inserting) + if supports_multi_insert? + execute insert_versions_sql(inserting) + else + inserting.each do |v| + execute insert_versions_sql(v) + end + end end end - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc: type = type.to_sym if type if native = native_database_types[type] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup @@ -1064,7 +1056,7 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end - elsif [:datetime, :time].include?(type) && precision ||= native[:precision] + elsif [:datetime, :timestamp, :time, :interval].include?(type) && precision ||= native[:precision] if (0..6) === precision column_type_sql << "(#{precision})" else @@ -1116,18 +1108,14 @@ module ActiveRecord end def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: - if column_name.is_a?(String) && /\W/ === column_name - column_names = column_name - else - column_names = Array(column_name) - end + column_names = index_column_names(column_name) options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) - index_name ||= index_name(table_name, index_name_options(column_names)) + index_name ||= index_name(table_name, column_names) if options.key?(:algorithm) algorithm = index_algorithms.fetch(options[:algorithm]) { @@ -1143,7 +1131,7 @@ module ActiveRecord validate_index_length!(table_name, index_name, options.fetch(:internal, false)) - if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") @@ -1165,12 +1153,16 @@ module ActiveRecord raise NotImplementedError, "#{self.class} does not support changing column comments" end - protected + private + def column_options_keys + [:limit, :precision, :scale, :default, :null, :collation, :comment] + 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? } @@ -1192,28 +1184,24 @@ module ActiveRecord def quoted_columns_for_index(column_names, **options) return [column_names] if column_names.is_a?(String) - quoted_columns = Hash[column_names.map { |name| [name, quote_column_name(name).dup] }] + quoted_columns = Hash[column_names.map { |name| [name.to_sym, quote_column_name(name).dup] }] add_options_for_index_columns(quoted_columns, options).values end def index_name_for_remove(table_name, options = {}) return options[:name] if can_remove_index_by_name?(options) - # if the adapter doesn't support the indexes call the best we can do - # is return the default index name for the options provided - return index_name(table_name, options) unless respond_to?(:indexes) - checks = [] if options.is_a?(Hash) checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name) - column_names = Array(options[:column]).map(&:to_s) + column_names = index_column_names(options[:column]) else - column_names = Array(options).map(&:to_s) + column_names = index_column_names(options) end - if column_names.any? - checks << lambda { |i| i.columns.join("_and_") == column_names.join("_and_") } + if column_names.present? + checks << lambda { |i| index_name(table_name, i.columns) == index_name(table_name, column_names) } end raise ArgumentError, "No name or columns specified" if checks.none? @@ -1252,7 +1240,10 @@ module ActiveRecord end end - private + def schema_creation + SchemaCreation.new(self) + end + def create_table_definition(*args) TableDefinition.new(*args) end @@ -1261,23 +1252,61 @@ module ActiveRecord AlterTable.new create_table_definition(name) end - def index_name_options(column_names) # :nodoc: - if column_names.is_a?(String) + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + + def index_column_names(column_names) + if column_names.is_a?(String) && /\W/.match?(column_names) + column_names + else + Array(column_names) + end + end + + def index_name_options(column_names) + if column_names.is_a?(String) && /\W/.match?(column_names) column_names = column_names.scan(/\w+/).join("_") end { column: column_names } end - def foreign_key_name(table_name, options) # :nodoc: - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + def foreign_key_name(table_name, options) options.fetch(:name) do + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "fk_rails_#{hashed_identifier}" end end - def validate_index_length!(table_name, new_name, internal = false) # :nodoc: + def foreign_key_for(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, options_or_to_table = {}) + foreign_key_for(from_table, options_or_to_table) || \ + raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}") + end + + def extract_foreign_key_action(specifier) + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify + when "RESTRICT"; :restrict + end + end + + def validate_index_length!(table_name, new_name, internal = false) max_index_length = internal ? index_name_length : allowed_index_name_length if new_name.length > max_index_length @@ -1296,6 +1325,27 @@ module ActiveRecord def can_remove_index_by_name?(options) options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + + def insert_versions_sql(versions) + sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) + + if versions.is_a?(Array) + sql = "INSERT INTO #{sm_table} (version) VALUES\n" + sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") + sql << ";\n\n" + sql + else + "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});" + end + end + + def data_source_sql(name = nil, type: nil) + raise NotImplementedError + end + + def quoted_scope(name = nil, type: nil) + raise NotImplementedError + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 6bb072dd73..19b7821494 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -149,57 +149,67 @@ module ActiveRecord end def begin_transaction(options = {}) - run_commit_callbacks = !current_transaction.joinable? - transaction = - if @stack.empty? - RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) - else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, - run_commit_callbacks: run_commit_callbacks) - end + @connection.lock.synchronize do + run_commit_callbacks = !current_transaction.joinable? + transaction = + if @stack.empty? + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) + end - @stack.push(transaction) - transaction + @stack.push(transaction) + transaction + end end def commit_transaction - transaction = @stack.last + @connection.lock.synchronize do + transaction = @stack.last - begin - transaction.before_commit_records - ensure - @stack.pop - end + begin + transaction.before_commit_records + ensure + @stack.pop + end - transaction.commit - transaction.commit_records + transaction.commit + transaction.commit_records + end end def rollback_transaction(transaction = nil) - transaction ||= @stack.pop - transaction.rollback - transaction.rollback_records + @connection.lock.synchronize do + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records + end end def within_new_transaction(options = {}) - transaction = begin_transaction options - yield - rescue Exception => error - if transaction - rollback_transaction - after_failure_actions(transaction, error) - end - raise - ensure - unless error - if Thread.current.status == "aborting" - rollback_transaction if transaction - else - begin - commit_transaction - rescue Exception - rollback_transaction(transaction) unless transaction.state.completed? - raise + @connection.lock.synchronize do + begin + transaction = begin_transaction options + yield + rescue Exception => error + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end + raise + ensure + unless error + if Thread.current.status == "aborting" + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0c7197a002..85d6fbe8b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -62,19 +62,19 @@ module ActiveRecord # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter ADAPTER_NAME = "Abstract".freeze + include ActiveSupport::Callbacks + define_callbacks :checkout, :checkin + include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache - include ActiveSupport::Callbacks include ColumnDumper include Savepoints SIMPLE_INT = /\A\d+\z/ - define_callbacks :checkout, :checkin - attr_accessor :visitor, :pool - attr_reader :schema_cache, :owner, :logger + attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner def self.type_cast_config_to_integer(config) @@ -93,8 +93,6 @@ module ActiveRecord end end - attr_reader :prepared_statements - def initialize(connection, logger = nil, config = {}) # :nodoc: super() @@ -106,7 +104,8 @@ module ActiveRecord @pool = nil @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} - @visitor = arel_visitor + @visitor = arel_visitor + @lock = Monitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -141,24 +140,8 @@ module ActiveRecord end end - def collector - if prepared_statements - SQLString.new - else - BindCollector.new - end - end - - def arel_visitor # :nodoc: - Arel::Visitors::ToSql.new(self) - end - - def valid_type?(type) - false - end - - def schema_creation - SchemaCreation.new self + def valid_type?(type) # :nodoc: + !native_database_types[type].nil? end # this method must only be called while holding connection pool's mutex @@ -168,7 +151,7 @@ module ActiveRecord if @owner == Thread.current msg << "it is already leased by the current thread." else - msg << "it is already in use by a different thread: #{@owner}. " << + msg << "it is already in use by a different thread: #{@owner}. " \ "Current thread: #{Thread.current}." end raise ActiveRecordError, msg @@ -186,8 +169,8 @@ module ActiveRecord def expire if in_use? if @owner != Thread.current - raise ActiveRecordError, "Cannot expire connection, " << - "it is owned by a different thread: #{@owner}. " << + raise ActiveRecordError, "Cannot expire connection, " \ + "it is owned by a different thread: #{@owner}. " \ "Current thread: #{Thread.current}." end @@ -223,16 +206,15 @@ module ActiveRecord self.class::ADAPTER_NAME end - # Does this adapter support migrations? - def supports_migrations? - false + def supports_migrations? # :nodoc: + true end + deprecate :supports_migrations? - # Can this adapter determine the primary key for tables not attached - # to an Active Record class, such as join tables? - def supports_primary_key? - false + def supports_primary_key? # :nodoc: + true end + deprecate :supports_primary_key? # Does this adapter support DDL rollbacks in transactions? That is, would # CREATE TABLE or ALTER TABLE get rolled back by a transaction? @@ -302,6 +284,12 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints + # in the same statement as creating the table? + def supports_foreign_keys_in_create? + supports_foreign_keys? + end + # Does this adapter support views? def supports_views? false @@ -332,6 +320,11 @@ module ActiveRecord true end + # Does this adapter support virtual columns? + def supports_virtual_columns? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -420,12 +413,15 @@ module ActiveRecord # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) + if ignored.size > 0 + ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.") + end reconnect! unless active? end # Provides access to the underlying database driver for this adapter. For # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, - # and a PGconn object in case of PostgreSQLAdapter. + # and a PG::Connection object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as # PostgreSQL's lo_* methods. @@ -433,15 +429,15 @@ module ActiveRecord @connection end - def case_sensitive_comparison(table, attribute, column, value) - table[attribute].eq(Arel::Nodes::BindParam.new) + def case_sensitive_comparison(table, attribute, column, value) # :nodoc: + table[attribute].eq(value) end - def case_insensitive_comparison(table, attribute, column, value) + def case_insensitive_comparison(table, attribute, column, value) # :nodoc: if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) + table[attribute].lower.eq(table.lower(value)) else - table[attribute].eq(Arel::Nodes::BindParam.new) + table[attribute].eq(value) end end @@ -461,14 +457,6 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: - Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation) - end - - def lookup_cast_type(sql_type) # :nodoc: - type_map.lookup(sql_type) - end - def column_name_for_operation(operation, node) # :nodoc: visitor.accept(node, collector).value end @@ -491,9 +479,13 @@ module ActiveRecord result end - protected + def default_index_type?(index) # :nodoc: + index.using.nil? + end + + private - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) register_class_with_limit m, %r(boolean)i, Type::Boolean register_class_with_limit m, %r(char)i, Type::String register_class_with_limit m, %r(binary)i, Type::Binary @@ -524,37 +516,37 @@ module ActiveRecord end end - def reload_type_map # :nodoc: + def reload_type_map type_map.clear initialize_type_map(type_map) end - def register_class_with_limit(mapping, key, klass) # :nodoc: + def register_class_with_limit(mapping, key, klass) mapping.register_type(key) do |*args| limit = extract_limit(args.last) klass.new(limit: limit) end end - def register_class_with_precision(mapping, key, klass) # :nodoc: + def register_class_with_precision(mapping, key, klass) mapping.register_type(key) do |*args| precision = extract_precision(args.last) klass.new(precision: precision) end end - def extract_scale(sql_type) # :nodoc: + def extract_scale(sql_type) case sql_type when /\((\d+)\)/ then 0 when /\((\d+)(,(\d+))\)/ then $3.to_i end end - def extract_precision(sql_type) # :nodoc: + def extract_precision(sql_type) $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ end - def extract_limit(sql_type) # :nodoc: + def extract_limit(sql_type) case sql_type when /^bigint/i 8 @@ -575,7 +567,7 @@ module ActiveRecord exception end - def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) + def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc: @instrumenter.instrument( "sql.active_record", sql: sql, @@ -583,25 +575,46 @@ module ActiveRecord binds: binds, type_casted_binds: type_casted_binds, statement_name: statement_name, - connection_id: object_id) { yield } + connection_id: object_id) do + @lock.synchronize do + yield + end + end rescue => e raise translate_exception_class(e, sql) end def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message) + case exception + when RuntimeError + exception + else + ActiveRecord::StatementInvalid.new(message) + end end def without_prepared_statement?(binds) !prepared_statements || binds.empty? end - def column_for(table_name, column_name) # :nodoc: + def column_for(table_name, column_name) column_name = column_name.to_s columns(table_name).detect { |c| c.name == column_name } || raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") end + + def collector + if prepared_statements + SQLString.new + else + BindCollector.new + end + end + + def arel_visitor + Arel::Visitors::ToSql.new(self) + end end end end 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 be8511f119..01599985ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,29 +6,22 @@ require "active_record/connection_adapters/mysql/quoting" require "active_record/connection_adapters/mysql/schema_creation" require "active_record/connection_adapters/mysql/schema_definitions" require "active_record/connection_adapters/mysql/schema_dumper" +require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" require "active_support/core_ext/string/strip" -require "active_support/core_ext/regexp" module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter include MySQL::Quoting include MySQL::ColumnDumper + include MySQL::SchemaStatements def update_table_definition(table_name, base) # :nodoc: MySQL::Table.new(table_name, base) end - def schema_creation # :nodoc: - MySQL::SchemaCreation.new(self) - end - - def arel_visitor # :nodoc: - Arel::Visitors::MySQL.new(self) - end - ## # :singleton-method: # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> @@ -36,17 +29,17 @@ module ActiveRecord # to your application.rb file: # # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false - class_attribute :emulate_booleans - self.emulate_booleans = true + class_attribute :emulate_booleans, default: true NATIVE_DATABASE_TYPES = { - primary_key: "int auto_increment PRIMARY KEY", + primary_key: "bigint auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, text: { name: "text", limit: 65535 }, integer: { name: "int", limit: 4 }, float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, + timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob", limit: 65535 }, @@ -54,9 +47,6 @@ module ActiveRecord json: { name: "json" }, } - INDEX_TYPES = [:fulltext, :spatial] - INDEX_USINGS = [:btree, :hash] - class StatementPool < ConnectionAdapters::StatementPool private def dealloc(stmt) stmt[:stmt].close @@ -68,19 +58,11 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - if version < "5.0.0" - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." + if version < "5.1.10" + raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10." end end - CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] - - def internal_string_options_for_primary_key # :nodoc: - super.tap { |options| - options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) - } - end - def version #:nodoc: @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end @@ -89,29 +71,12 @@ module ActiveRecord /mariadb/i.match?(full_version) end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - - def supports_primary_key? - true - end - def supports_bulk_alter? #:nodoc: true end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Technically MySQL allows to create indexes with the sort order syntax - # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? - true + !mariadb? && version >= "8.0.1" end def supports_transaction_isolation? @@ -142,6 +107,14 @@ module ActiveRecord end end + def supports_virtual_columns? + if mariadb? + version >= "5.2.0" + else + version >= "5.7.5" + end + end + def supports_advisory_locks? true end @@ -170,10 +143,6 @@ module ActiveRecord raise NotImplementedError end - def new_column(*args) #:nodoc: - MySQL::Column.new(*args) - end - # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: @@ -216,7 +185,11 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - log(sql, name) { @connection.query(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.query(sql) + end + end end # Mysql2Adapter doesn't have to free a result after using it, but we use this method @@ -307,116 +280,18 @@ module ActiveRecord show_variable "collation_database" end - def tables(name = nil) # :nodoc: - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #tables currently returns both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only return tables. - Use #data_sources instead. - MSG - - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end - - data_sources - end - - def data_sources - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(@config[:database])}" - - select_values(sql, "SCHEMA") - end - def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - def table_exists?(table_name) - # Update lib/active_record/internal_metadata.rb when this gets removed - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #table_exists? currently checks both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only check tables. - Use #data_source_exists? instead. - MSG - - data_source_exists?(table_name) - end - - def data_source_exists?(table_name) - return false unless table_name.present? - - schema, name = extract_schema_qualified_name(table_name) - - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - def views # :nodoc: - select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - schema, name = extract_schema_qualified_name(view_name) - - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" - sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - indexes = [] - current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| - each_hash(result) do |row| - if current_index != row[:Key_name] - next if row[:Key_name] == "PRIMARY" # skip the primary key - current_index = row[:Key_name] - - mysql_index_type = row[:Index_type].downcase.to_sym - index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil - index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence) - end - - indexes.last.columns << row[:Column_name] - indexes.last.lengths << row[:Sub_part] - end - end - - indexes - end - - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - column_definitions(table_name).map do |field| - type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" - default, default_function = nil, field[:Default] - else - default, default_function = field[:Default], nil - end - new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) - end - end - def table_comment(table_name) # :nodoc: - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT table_comment FROM information_schema.tables - WHERE table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + WHERE table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} SQL end @@ -509,14 +384,14 @@ module ActiveRecord end def add_sql_comment!(sql, comment) # :nodoc: - sql << " COMMENT #{quote(comment)}" if comment + sql << " COMMENT #{quote(comment)}" if comment.present? sql end def foreign_keys(table_name) raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', @@ -529,8 +404,9 @@ module ActiveRecord JOIN information_schema.referential_constraints rc USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL - AND fk.table_schema = #{quote(schema)} - AND fk.table_name = #{quote(name)} + AND fk.table_schema = #{scope[:schema]} + AND fk.table_name = #{scope[:name]} + AND rc.table_name = #{scope[:name]} SQL fk_info.map do |row| @@ -569,7 +445,7 @@ module ActiveRecord end # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc: sql = \ case type.to_s when "integer" @@ -585,7 +461,7 @@ module ActiveRecord binary_to_sql(limit) end else - super(type, limit, precision, scale) + super end sql << " unsigned" if unsigned && type != :primary_key @@ -602,21 +478,21 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' - AND table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + AND table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} ORDER BY ordinal_position SQL end - def case_sensitive_comparison(table, attribute, column, value) + def case_sensitive_comparison(table, attribute, column, value) # :nodoc: if column.collation && !column.case_sensitive? - table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) + table[attribute].eq(Arel::Nodes::Bin.new(value)) else super end @@ -646,13 +522,13 @@ module ActiveRecord self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end - def valid_type?(type) - !native_database_types[type].nil? + def default_index_type?(index) # :nodoc: + index.using == :btree || super end - protected + private - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) super register_class_with_limit m, %r(char)i, MysqlString @@ -692,9 +568,9 @@ module ActiveRecord end end - def register_integer_type(mapping, key, options) # :nodoc: + def register_integer_type(mapping, key, options) mapping.register_type(key) do |sql_type| - if /\bunsigned\z/ === sql_type + if /\bunsigned\b/.match?(sql_type) Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -703,21 +579,18 @@ module ActiveRecord end def extract_precision(sql_type) - if /time/ === sql_type + if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type) super || 0 else super end end - def fetch_type_metadata(sql_type, extra = "") - MySQL::TypeMetadata.new(super(sql_type), extra: extra) - 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})" } @@ -734,9 +607,14 @@ module ActiveRecord # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 + ER_NOT_NULL_VIOLATION = 1048 + ER_DO_NOT_HAVE_DEFAULT = 1364 ER_NO_REFERENCED_ROW_2 = 1452 ER_DATA_TOO_LONG = 1406 + ER_OUT_OF_RANGE = 1264 ER_LOCK_DEADLOCK = 1213 + ER_CANNOT_ADD_FOREIGN = 1215 + ER_CANNOT_CREATE_TABLE = 1005 def translate_exception(exception, message) case error_number(exception) @@ -744,8 +622,20 @@ module ActiveRecord RecordNotUnique.new(message) when ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) + when ER_CANNOT_ADD_FOREIGN + mismatched_foreign_key(message) + when ER_CANNOT_CREATE_TABLE + if message.include?("errno: 150") + mismatched_foreign_key(message) + else + super + end when ER_DATA_TOO_LONG ValueTooLong.new(message) + when ER_OUT_OF_RANGE + RangeError.new(message) + when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT + NotNullViolation.new(message) when ER_LOCK_DEADLOCK Deadlocked.new(message) else @@ -762,14 +652,18 @@ module ActiveRecord def change_column_sql(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) - unless options_include_default?(options) + unless options.key?(:default) options[:default] = column.default end - unless options.has_key?(:null) + unless options.key?(:null) options[:null] = column.null end + unless options.key?(:comment) + options[:comment] = column.comment + end + td = create_table_definition(table_name) cd = td.new_column_definition(column.name, type, options) schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) @@ -816,21 +710,17 @@ module ActiveRecord [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end - private - # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] + subselect = select.clone + subselect.projections = [key] # Materialize subquery by adding distinct # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subsubselect.distinct unless select.limit || select.offset || select.orders.any? + subselect.distinct unless select.limit || select.offset || select.orders.any? - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - subselect.from subsubselect.as("__active_record_temp") + Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name)) end def supports_rename_index? @@ -844,9 +734,9 @@ module ActiveRecord variables["sql_auto_is_null"] = 0 # Increase timeout so the server doesn't disconnect us. - wait_timeout = @config[:wait_timeout] + wait_timeout = self.class.type_cast_config_to_integer(@config[:wait_timeout]) wait_timeout = 2147483 unless wait_timeout.is_a?(Integer) - variables["wait_timeout"] = self.class.type_cast_config_to_integer(wait_timeout) + variables["wait_timeout"] = wait_timeout defaults = [":default", :default].to_set @@ -887,7 +777,7 @@ module ActiveRecord end.compact.join(", ") # ...and send them all in one query - @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" + execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" end def column_definitions(table_name) # :nodoc: @@ -896,25 +786,24 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "CASCADE"; :cascade - when "SET NULL"; :nullify - end - end - def create_table_info(table_name) # :nodoc: select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end - def create_table_definition(*args) # :nodoc: - MySQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::MySQL.new(self) end - def extract_schema_qualified_name(string) # :nodoc: - schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) - schema, name = @config[:database], schema unless name - [schema, name] + def mismatched_foreign_key(message) + parts = message.scan(/`(\w+)`[ $)]/).flatten + MismatchedForeignKey.new( + self, + message: message, + table: parts[0], + foreign_key: parts[1], + target_table: parts[2], + primary_key: parts[3], + ) end def integer_to_sql(limit) # :nodoc: @@ -949,11 +838,6 @@ module ActiveRecord end class MysqlJson < Type::Internal::AbstractJson # :nodoc: - def changed_in_place?(raw_old_value, new_value) - # Normalization is required because MySQL JSON data format includes - # the space between the elements. - super(serialize(deserialize(raw_old_value)), new_value) - end end class MysqlString < Type::String # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1808173592..61cd7ae4cc 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -29,7 +29,7 @@ module ActiveRecord end def bigint? - /\Abigint\b/ === sql_type + /\Abigint\b/.match?(sql_type) end # Returns the human name of the column name. @@ -40,6 +40,28 @@ module ActiveRecord Base.human_attribute_name(@name) end + def init_with(coder) + @name = coder["name"] + @table_name = coder["table_name"] + @sql_type_metadata = coder["sql_type_metadata"] + @null = coder["null"] + @default = coder["default"] + @default_function = coder["default_function"] + @collation = coder["collation"] + @comment = coder["comment"] + end + + def encode_with(coder) + coder["name"] = @name + coder["table_name"] = @table_name + coder["sql_type_metadata"] = @sql_type_metadata + coder["null"] = @null + coder["default"] = @default + coder["default_function"] = @default_function + coder["collation"] = @collation + coder["comment"] = @comment + end + def ==(other) other.is_a?(Column) && attributes_for_hash == other.attributes_for_hash diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 849130ba43..3e4ea28f63 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -48,8 +48,8 @@ module ActiveRecord # Converts the given URL to a full connection hash. def to_hash - config = raw_config.reject { |_,value| value.blank? } - config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String } + config = raw_config.reject { |_, value| value.blank? } + config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String } config end @@ -149,9 +149,18 @@ module ActiveRecord # Expands each key in @configurations hash into fully resolved hash def resolve_all config = configurations.dup + + if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) + end + + config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } + config.merge! env_config if env_config + config.each do |key, value| config[key] = resolve(value) if value end + config end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb index 296d9a15f8..c9ad47c035 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -5,16 +5,20 @@ module ActiveRecord delegate :extra, to: :sql_type_metadata, allow_nil: true def unsigned? - /\bunsigned\z/ === sql_type + /\bunsigned(?: zerofill)?\z/.match?(sql_type) end def case_sensitive? - collation && collation !~ /_ci\z/ + collation && !/_ci\z/.match?(collation) end def auto_increment? extra == "auto_increment" end + + def virtual? + /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 56800f7590..8c67a7a80b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -3,7 +3,7 @@ module ActiveRecord module MySQL module DatabaseStatements # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = [], preparable: nil) + def select_all(arel, name = nil, binds = [], preparable: nil) # :nodoc: result = if ExplainRegistry.collect? && prepared_statements unprepared_statement { super } else @@ -15,8 +15,8 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - select_result(sql, name, binds) do |result| + def select_rows(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| @connection.next_result while @connection.more_results? result.to_a end @@ -52,15 +52,15 @@ module ActiveRecord end alias :exec_update :exec_delete - protected + private def last_inserted_id(result) @connection.last_id end - private - - def select_result(sql, name = nil, binds = []) + def select_result(arel, name, binds) + arel, binds = binds_from_relation(arel, binds) + sql = to_sql(arel, binds) if without_prepared_statement?(binds) execute_and_free(sql, name) { |result| yield result } else @@ -86,7 +86,9 @@ module ActiveRecord end begin - result = stmt.execute(*type_casted_binds) + result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + stmt.execute(*type_casted_binds) + end rescue Mysql2::Error => e if cache_stmt @statements.delete(sql) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb index 925555703d..9691060cd3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb @@ -47,7 +47,7 @@ module ActiveRecord def build_separator(widths) padding = 1 - "+" + widths.map { |w| "-" * (w + (padding*2)) }.join("+") + "+" + "+" + widths.map { |w| "-" * (w + (padding * 2)) }.join("+") + "+" end def build_cells(items, widths) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index 9d11ad28d4..d4f5906b33 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -36,15 +36,9 @@ module ActiveRecord end end - private - - def _quote(value) - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end + def quoted_binary(value) + "x'#{value.hex}'" + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index d808b50332..083cd6340f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -1,9 +1,9 @@ module ActiveRecord module ConnectionAdapters module MySQL - class SchemaCreation < AbstractAdapter::SchemaCreation - delegate :add_sql_comment!, to: :@conn - private :add_sql_comment! + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: + delegate :add_sql_comment!, :mariadb?, to: :@conn + private :add_sql_comment!, :mariadb? private @@ -11,11 +11,6 @@ module ActiveRecord "DROP FOREIGN KEY #{name}" end - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) - super - end - def visit_AddColumnDefinition(o) add_column_position!(super, column_options(o.column)) end @@ -29,13 +24,15 @@ module ActiveRecord add_sql_comment!(super, options[:comment]) end - def column_options(o) - column_options = super - column_options[:charset] = o.charset - column_options - end - def add_column_options!(sql, options) + # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, + # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP + # column to contain NULL, explicitly declare it with the NULL attribute. + # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html + if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] + sql << " NULL" unless options[:null] == false || options_include_default?(options) + end + if charset = options[:charset] sql << " CHARACTER SET #{charset}" end @@ -44,6 +41,13 @@ module ActiveRecord sql << " COLLATE #{collation}" end + if as = options[:as] + sql << " AS (#{as})" + if options[:stored] + sql << (mariadb? ? " PERSISTENT" : " STORED") + end + end + add_sql_comment!(super, options[:comment]) end 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 ce773ed75b..6d88c14d50 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -3,7 +3,7 @@ module ActiveRecord module MySQL module ColumnMethods def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint && !options.key?(:default) + options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) super end @@ -56,32 +56,29 @@ module ActiveRecord end end - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset, :unsigned - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - def new_column_definition(name, type, options) # :nodoc: - column = super - case column.type + def new_column_definition(name, type, **options) # :nodoc: + case type + when :virtual + type = options[:type] when :primary_key - column.type = :integer - column.auto_increment = true + type = :integer + options[:limit] ||= 8 + options[:auto_increment] = true + options[:primary_key] = true when /\Aunsigned_(?<type>.+)\z/ - column.type = $~[:type].to_sym - column.unsigned = true + type = $~[:type].to_sym + options[:unsigned] = true end - column.unsigned ||= options[:unsigned] - column.charset = options[:charset] - column + + super end private - - def create_column_definition(name, type) - MySQL::ColumnDefinition.new(name, type) + def aliased_types(name, fallback) + fallback end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 39221eeb0c..e2ba0ba1a0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -1,21 +1,17 @@ module ActiveRecord module ConnectionAdapters module MySQL - module ColumnDumper - def column_spec_for_primary_key(column) - if column.bigint? - spec = { id: :bigint.inspect } - spec[:default] = schema_default(column) || "nil" unless column.auto_increment? - else - spec = super - end - spec[:unsigned] = "true" if column.unsigned? - spec - end - + module ColumnDumper # :nodoc: def prepare_column_options(column) spec = super spec[:unsigned] = "true" if column.unsigned? + + if supports_virtual_columns? && column.virtual? + spec[:as] = extract_expression_for_virtual_column(column) + spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) + spec = { type: schema_type(column).inspect }.merge!(spec) + end + spec end @@ -26,11 +22,18 @@ module ActiveRecord private def default_primary_key?(column) - super && column.auto_increment? + super && column.auto_increment? && !column.unsigned? + end + + def explicit_primary_key_default?(column) + column.type == :integer && !column.auto_increment? end def schema_type(column) - if column.sql_type == "tinyblob" + case column.sql_type + when /\Atimestamp\b/ + :timestamp + when "tinyblob" :blob else super @@ -38,7 +41,7 @@ module ActiveRecord end def schema_precision(column) - super unless /time/ === column.sql_type && column.precision == 0 + super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0 end def schema_collation(column) @@ -48,6 +51,22 @@ module ActiveRecord column.collation.inspect if column.collation != @table_collation_cache[table_name] end end + + def extract_expression_for_virtual_column(column) + if mariadb? + create_table_info = create_table_info(column.table_name) + if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info + $~[:expression].inspect + end + else + scope = quoted_scope(column.table_name) + sql = "SELECT generation_expression FROM information_schema.columns" \ + " WHERE table_schema = #{scope[:schema]}" \ + " AND table_name = #{scope[:name]}" \ + " AND column_name = #{quote(column.name)}" + select_value(sql, "SCHEMA").inspect + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb new file mode 100644 index 0000000000..f9e1e046ea --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + indexes = [] + current_index = nil + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| + each_hash(result) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == "PRIMARY" # skip the primary key + current_index = row[:Key_name] + + mysql_index_type = row[:Index_type].downcase.to_sym + case mysql_index_type + when :fulltext, :spatial + index_type = mysql_index_type + when :btree, :hash + index_using = mysql_index_type + end + + indexes << IndexDefinition.new( + row[:Table], + row[:Key_name], + row[:Non_unique].to_i == 0, + 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" + end + end + + indexes + end + + def internal_string_options_for_primary_key + super.tap do |options| + if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0") + options[:collation] = collation.sub(/\A[^_]+/, "utf8") + end + end + end + + private + CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] + + def schema_creation + MySQL::SchemaCreation.new(self) + end + + def create_table_definition(*args) + MySQL::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + default, default_function = nil, field[:Default] + else + default, default_function = field[:Default], nil + end + + MySQL::Column.new( + field[:Field], + default, + type_metadata, + field[:Null] == "YES", + table_name, + default_function, + field[:Collation], + comment: field[:Comment].presence + ) + end + + def fetch_type_metadata(sql_type, extra = "") + MySQL::TypeMetadata.new(super(sql_type), extra: extra) + end + + def extract_foreign_key_action(specifier) + super unless specifier == "RESTRICT" + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + + sql = "SELECT table_name FROM information_schema.tables" + sql << " WHERE table_schema = #{scope[:schema]}" + sql << " AND table_name = #{scope[:name]}" if scope[:name] + sql << " AND table_type = #{scope[:type]}" if scope[:type] + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + scope = {} + scope[:schema] = schema ? quote(schema) : "database()" + scope[:name] = quote(name) if name + scope[:type] = quote(type) if type + scope + end + + def extract_schema_qualified_name(string) + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = nil, schema unless name + [schema, name] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb index 24dcf852e1..9ad6a6c0d0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -2,6 +2,8 @@ module ActiveRecord module ConnectionAdapters module MySQL class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + attr_reader :extra def initialize(type_metadata, extra: "") diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index a3e2c913c5..af55cfe2f6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -10,16 +10,12 @@ module ActiveRecord # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) config = config.symbolize_keys - - config[:username] = "root" if config[:username].nil? config[:flags] ||= 0 - if Mysql2::Client.const_defined? :FOUND_ROWS - if config[:flags].kind_of? Array - config[:flags].push "FOUND_ROWS".freeze - else - config[:flags] |= Mysql2::Client::FOUND_ROWS - end + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS end client = Mysql2::Client.new(config) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 092543259f..705e6063dc 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -7,18 +7,14 @@ module ActiveRecord PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds)) end - def select_value(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| + def select_value(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 end end - def select_values(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| + def select_values(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| if result.nfields > 0 result.column_values(0) else @@ -29,8 +25,8 @@ module ActiveRecord # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. - def select_rows(sql, name = nil, binds = []) - execute_and_clear(sql, name, binds) do |result| + def select_rows(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| result.values end end @@ -85,7 +81,9 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: log(sql, name) do - result_as_array @connection.async_exec(sql) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + result_as_array @connection.async_exec(sql) + end end end @@ -95,7 +93,9 @@ module ActiveRecord # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) log(sql, name) do - @connection.async_exec(sql) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.async_exec(sql) + end end end @@ -130,7 +130,7 @@ module ActiveRecord super end - protected :sql_for_insert + private :sql_for_insert def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) if use_insert_returning? || pk == false @@ -175,6 +175,14 @@ module ActiveRecord def suppress_composite_primary_key(pk) pk unless pk.is_a?(Array) end + + def select_result(arel, name, binds) + arel, binds = binds_from_relation(arel, binds) + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + yield result + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 0e526f6201..4098250f3e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -11,6 +11,7 @@ require "active_record/connection_adapters/postgresql/oid/inet" require "active_record/connection_adapters/postgresql/oid/json" require "active_record/connection_adapters/postgresql/oid/jsonb" require "active_record/connection_adapters/postgresql/oid/money" +require "active_record/connection_adapters/postgresql/oid/oid" require "active_record/connection_adapters/postgresql/oid/point" require "active_record/connection_adapters/postgresql/oid/legacy_point" require "active_record/connection_adapters/postgresql/oid/range" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index b969503178..a73a8c1726 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,8 +5,10 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable + Data = Struct.new(:encoder, :values) # :nodoc: + attr_reader :subtype, :delimiter - delegate :type, :user_input_in_time_zone, :limit, to: :subtype + delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype def initialize(subtype, delimiter = ",") @subtype = subtype @@ -17,8 +19,11 @@ module ActiveRecord end def deserialize(value) - if value.is_a?(::String) + case value + when ::String type_cast_array(@pg_decoder.decode(value), :deserialize) + when Data + type_cast_array(value.values, :deserialize) else super end @@ -33,11 +38,8 @@ module ActiveRecord def serialize(value) if value.is_a?(::Array) - result = @pg_encoder.encode(type_cast_array(value, :serialize)) - if encoding = determine_encoding_of_strings(value) - result.encode!(encoding) - end - result + casted_values = type_cast_array(value, :serialize) + Data.new(@pg_encoder, casted_values) else super end @@ -58,6 +60,10 @@ module ActiveRecord value.map(&block) end + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + private def type_cast_array(value, method) @@ -67,13 +73,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def determine_encoding_of_strings(value) - case value - when ::Array then determine_encoding_of_strings(value.first) - when ::String then value.encoding - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 74bff229ea..0a505f46a7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -34,13 +34,15 @@ module ActiveRecord end def binary? - /\A[01]*\Z/ === value + /\A[01]*\Z/.match?(value) end def hex? - /\A[0-9A-F]*\Z/i === value + /\A[0-9A-F]*\Z/i.match?(value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :value diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 8f9d6e7f9b..702fa8175c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -6,7 +6,7 @@ module ActiveRecord def deserialize(value) return if value.nil? return value.to_s if value.is_a?(Type::Binary::Data) - PGconn.unescape_bytea(super) + PG::Connection.unescape_bytea(super) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 2d3e6a925d..49dd4fc73f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -12,8 +12,8 @@ module ActiveRecord def deserialize(value) if value.is_a?(::String) ::Hash[value.scan(HstorePair).map { |k, v| - v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1') [k, v] }] else @@ -24,6 +24,8 @@ module ActiveRecord def serialize(value) if value.is_a?(::Hash) value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(", ") + elsif value.respond_to?(:to_unsafe_h) + serialize(value.to_unsafe_h) else value end @@ -33,6 +35,14 @@ module ActiveRecord ActiveRecord::Store::StringKeyedHashAccessor end + # Will compare the Hash equivalents of +raw_old_value+ and +new_value+. + # By comparing hashes, this avoids an edge case where the order of + # the keys change between the two hashes, and they would not be marked + # as equal. + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + private HstorePair = begin diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 87391b5dc7..705cb7f0b3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -6,16 +6,6 @@ module ActiveRecord def type :jsonb end - - def changed_in_place?(raw_old_value, new_value) - # Postgres does not preserve insignificant whitespaces when - # round-tripping jsonb columns. This causes some false positives for - # the comparison here. Therefore, we need to parse and re-dump the - # raw value here to ensure the insignificant whitespaces are - # consistent with our encoder's output. - raw_old_value = serialize(deserialize(raw_old_value)) - super(raw_old_value, new_value) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb new file mode 100644 index 0000000000..9c2ac08b30 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Oid < Type::Integer # :nodoc: + def type + :oid + end + end + end + 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 2c714f4018..54d5d0902e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/filters" - module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 2d2fede4e8..564e82a4ac 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -5,8 +5,9 @@ module ActiveRecord class SpecializedString < Type::String # :nodoc: attr_reader :type - def initialize(type) + def initialize(type, **options) @type = type + super(options) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b5031d890f..44eb666965 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -33,7 +33,7 @@ module ActiveRecord # Quotes schema names for use in SQL queries. def quote_schema_name(name) - PGconn.quote_ident(name) + PG::Connection.quote_ident(name) end def quote_table_name_for_assignment(table, attr) @@ -42,7 +42,7 @@ module ActiveRecord # Quotes column names for use in SQL queries. def quote_column_name(name) # :nodoc: - @quoted_column_names[name] ||= PGconn.quote_ident(super).freeze + @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze end # Quote date/time values for use in SQL input. @@ -55,10 +55,14 @@ module ActiveRecord end end + def quoted_binary(value) # :nodoc: + "'#{escape_bytea(value.to_s)}'" + end + def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && value.include?("()") + elsif column.type == :uuid && /\(\)/.match?(value) value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) @@ -73,11 +77,12 @@ module ActiveRecord end private + def lookup_cast_type(sql_type) + super(select_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) + end def _quote(value) case value - when Type::Binary::Data - "'#{escape_bytea(value.to_s)}'" when OID::Xml::Data "xml '#{quote_string(value.to_s)}'" when OID::Bit::Data @@ -92,6 +97,8 @@ module ActiveRecord else super end + when OID::Array::Data + _quote(encode_array(value)) else super end @@ -101,15 +108,42 @@ module ActiveRecord case value when Type::Binary::Data # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } when OID::Xml::Data, OID::Bit::Data value.to_s + when OID::Array::Data + encode_array(value) else super end end + + def encode_array(array_data) + encoder = array_data.encoder + values = type_cast_array(array_data.values) + + result = encoder.encode(values) + if encoding = determine_encoding_of_strings_in_array(values) + result.force_encoding(encoding) + end + result + end + + def determine_encoding_of_strings_in_array(value) + case value + when ::Array then determine_encoding_of_strings_in_array(value.first) + when ::String then value.encoding + end + end + + def type_cast_array(values) + case values + when ::Array then values.map { |item| type_cast_array(item) } + else _type_cast(values) + end + 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 new file mode 100644 index 0000000000..e1d5089115 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: + private + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end + end + end + end +end 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 a11dbe7dce..11ea1e5144 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -11,11 +11,22 @@ module ActiveRecord # t.timestamps # end # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: + # By default, this will use the +gen_random_uuid()+ function from the + # +pgcrypto+ extension. As that extension is only available in + # PostgreSQL 9.4+, for earlier versions an explicit default can be set + # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: "uuid_generate_v4()" + # t.uuid :foo_id + # t.timestamps + # end + # + # To enable the appropriate extension, which is a requirement, use + # the +enable_extension+ method in your migrations. + # + # To use a UUID primary key without any of the extensions, set the + # +:default+ option to +nil+: # # create_table :stuffs, id: false do |t| # t.primary_key :id, :uuid, default: nil @@ -23,15 +34,25 @@ module ActiveRecord # t.timestamps # end # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. + # You may also pass a custom stored procedure that returns a UUID or use a + # different UUID generation function from another library. # # Note that setting the UUID primary key default value to +nil+ will # require you to assure that you always provide a UUID value before saving # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, **options) - options[:default] = options.fetch(:default, "uuid_generate_v4()") if type == :uuid + options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) + if type == :uuid + options[:default] = options.fetch(:default, "gen_random_uuid()") + elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type) + type = if type == :bigint || options[:limit] == 8 + :bigserial + else + :serial + end + end + super end @@ -67,6 +88,10 @@ module ActiveRecord args.each { |name| column(name, :inet, options) } end + def interval(*args, **options) + args.each { |name| column(name, :interval, options) } + end + def int4range(*args, **options) args.each { |name| column(name, :int4range, options) } end @@ -99,6 +124,10 @@ module ActiveRecord args.each { |name| column(name, :numrange, options) } end + def oid(*args, **options) + args.each { |name| column(name, :oid, options) } + end + def point(*args, **options) args.each { |name| column(name, :point, options) } end @@ -152,24 +181,8 @@ module ActiveRecord end end - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - - def new_column_definition(name, type, options) # :nodoc: - column = super - column.array = options[:array] - column - end - - private - - def create_column_definition(name, type) - PostgreSQL::ColumnDefinition.new name, type - end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index c20baf655c..5555a46b6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -1,15 +1,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL - module ColumnDumper - def column_spec_for_primary_key(column) - spec = super - if schema_type(column) == :uuid - spec[:default] ||= "nil" - end - spec - end - + module ColumnDumper # :nodoc: # Adds +:array+ option to the default set def prepare_column_options(column) spec = super @@ -25,7 +17,11 @@ module ActiveRecord private def default_primary_key?(column) - schema_type(column) == :serial + schema_type(column) == :bigserial + end + + def explicit_primary_key_default?(column) + column.type == :uuid || (column.type == :integer && !column.serial?) end def schema_type(column) 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 29a77580f5..cb45d7ba6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -3,22 +3,6 @@ require "active_support/core_ext/string/strip" module ActiveRecord module ConnectionAdapters module PostgreSQL - class SchemaCreation < AbstractAdapter::SchemaCreation - private - - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) - super - end - - def add_column_options!(sql, options) - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - super - end - end - module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -70,91 +54,24 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end - # Returns the list of all tables in the schema search path. - def tables(name = nil) - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end - - select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", "SCHEMA") - end - - def data_sources # :nodoc - select_values(<<-SQL, "SCHEMA") - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND n.nspname = ANY (current_schemas(false)) - SQL - end - - # Returns true if table exists. - # If the schema is not specified as part of +name+ then it will only find tables within - # the current schema search path (regardless of permissions to access tables in other schemas) - def table_exists?(name) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #table_exists? currently checks both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only check tables. - Use #data_source_exists? instead. - MSG - - data_source_exists?(name) - end - - def data_source_exists?(name) - name = Utils.extract_schema_qualified_name(name.to_s) - return false unless name.identifier - - select_value(<<-SQL, "SCHEMA").to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND c.relname = '#{name.identifier}' - AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} - SQL - end - - def views # :nodoc: - select_values(<<-SQL, "SCHEMA") - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND n.nspname = ANY (current_schemas(false)) - SQL - end - - def view_exists?(view_name) # :nodoc: - name = Utils.extract_schema_qualified_name(view_name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND c.relname = '#{name.identifier}' - AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} - SQL - end - def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. def schema_exists?(name) - select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", "SCHEMA").to_i > 0 + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0 end # Verifies existence of an index with a given name. - def index_name_exists?(table_name, index_name, default) - table = Utils.extract_schema_qualified_name(table_name.to_s) - index = Utils.extract_schema_qualified_name(index_name.to_s) + def index_name_exists?(table_name, index_name, default = nil) + unless default.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing default to #index_name_exists? is deprecated without replacement. + MSG + end + table = quoted_scope(table_name) + index = quoted_scope(index_name) select_value(<<-SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) @@ -163,15 +80,21 @@ module ActiveRecord INNER JOIN pg_class i ON d.indexrelid = i.oid LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index.identifier}' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} + AND i.relname = #{index[:name]} + AND t.relname = #{table[:name]} + AND n.nspname = #{index[:schema]} SQL end # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - table = Utils.extract_schema_qualified_name(table_name.to_s) + def indexes(table_name, name = nil) # :nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + scope = quoted_scope(table_name) result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, @@ -185,8 +108,8 @@ module ActiveRecord LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' AND d.indisprimary = 'f' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} + AND t.relname = #{scope[:name]} + AND n.nspname = #{scope[:schema]} ORDER BY i.relname SQL @@ -217,49 +140,43 @@ module ActiveRecord ] end - IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) - end.compact - end - - # Returns the list of all column definitions for a table. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment| - oid = oid.to_i - fmod = fmod.to_i - type_metadata = fetch_type_metadata(column_name, type, oid, fmod) - default_value = extract_value_from_default(default) - default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment: comment.presence) + IndexDefinition.new( + table_name, + index_name, + unique, + columns, + orders: orders, + where: where, + using: using.to_sym, + comment: comment.presence + ) end end - def new_column(*args) # :nodoc: - PostgreSQLColumn.new(*args) - end - def table_options(table_name) # :nodoc: - { comment: table_comment(table_name) } + if comment = table_comment(table_name) + { comment: comment } + end end # Returns a comment stored in database for given table def table_comment(table_name) # :nodoc: - name = Utils.extract_schema_qualified_name(table_name.to_s) - if name.identifier + scope = quoted_scope(table_name, type: "BASE TABLE") + if scope[:name] select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relname = #{quote(name.identifier)} - AND c.relkind IN ('r') -- (r)elation/table - AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + WHERE c.relname = #{scope[:name]} + AND c.relkind IN (#{scope[:type]}) + AND n.nspname = #{scope[:schema]} SQL end end # Returns the current database name. def current_database - select_value("select current_database()", "SCHEMA") + select_value("SELECT current_database()", "SCHEMA") end # Returns the current schema name. @@ -269,17 +186,17 @@ module ActiveRecord # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns an array of schema names. @@ -373,9 +290,17 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) + max_pk = select_value("select MAX(#{quote_column_name pk}) from #{quote_table_name(table)}") + if max_pk.nil? + if postgresql_version >= 100000 + minvalue = select_value("SELECT seqmin from pg_sequence where seqrelid = '#{quoted_sequence}'::regclass") + else + minvalue = select_value("SELECT min_value FROM #{quoted_sequence}") + end + end select_value(<<-end_sql, "SCHEMA") - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + SELECT setval('#{quoted_sequence}', #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false}) end_sql end end @@ -436,16 +361,17 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: select_values(<<-SQL.strip_heredoc, "SCHEMA") - WITH pk_constraint AS ( - SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint - WHERE contype = 'p' - AND conrelid = '#{quote_table_name(table_name)}'::regclass - ), cons AS ( - SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint - ) - SELECT attr.attname FROM pg_attribute attr - INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum - ORDER BY cons.rownum + SELECT a.attname + FROM ( + SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx + FROM pg_index + WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass + AND indisprimary + ) i + JOIN pg_attribute a + ON a.attrelid = i.indrelid + AND a.attnum = i.indkey[i.idx] + ORDER BY i.idx SQL end @@ -459,14 +385,15 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq && seq.identifier == "#{table_name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" + if pk idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" + end end - rename_table_indexes(table_name, new_name) end @@ -480,7 +407,7 @@ module ActiveRecord 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[:limit], options[:precision], options[:scale], options[:array]) + sql_type = type_to_sql(type, options) sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" @@ -488,12 +415,12 @@ module ActiveRecord if options[:using] sql << " USING #{options[:using]}" elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array]) + 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_include_default?(options) + 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) end @@ -583,6 +510,7 @@ module ActiveRecord end def foreign_keys(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-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 FROM pg_constraint c @@ -592,8 +520,8 @@ module ActiveRecord JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid JOIN pg_namespace t3 ON c.connamespace = t3.oid WHERE c.contype = 'f' - AND t1.relname = #{quote(table_name)} - AND t3.nspname = ANY (current_schemas(false)) + AND t1.relname = #{scope[:name]} + AND t3.nspname = #{scope[:schema]} ORDER BY c.conname SQL @@ -611,20 +539,8 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "c"; :cascade - when "n"; :nullify - when "r"; :restrict - end - end - - def index_name_length - 63 - end - # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ case type.to_s when "binary" @@ -649,7 +565,7 @@ module ActiveRecord else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") end else - super(type, limit, precision, scale) + super end sql << "[]" if array && type != :primary_key @@ -670,17 +586,84 @@ module ActiveRecord [super, *order_columns].join(", ") end - def fetch_type_metadata(column_name, sql_type, oid, fmod) - cast_type = get_oid_type(oid, fmod, column_name, sql_type) - simple_type = SqlTypeMetadata.new( - sql_type: sql_type, - type: cast_type.type, - limit: cast_type.limit, - precision: cast_type.precision, - scale: cast_type.scale, - ) - PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) - end + private + def schema_creation + PostgreSQL::SchemaCreation.new(self) + end + + def create_table_definition(*args) + PostgreSQL::TableDefinition.new(*args) + 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) + default_value = extract_value_from_default(default) + default_function = extract_default_function(default_value, default) + + PostgreSQLColumn.new( + column_name, + default_value, + type_metadata, + !notnull, + table_name, + default_function, + collation, + comment: comment.presence + ) + end + + def fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end + + def extract_foreign_key_action(specifier) + case specifier + when "c"; :cascade + when "n"; :nullify + when "r"; :restrict + end + 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 + + sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" + sql << " WHERE n.nspname = #{scope[:schema]}" + sql << " AND c.relname = #{scope[:name]}" if scope[:name] + sql << " AND c.relkind IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + type = \ + case type + when "BASE TABLE" + "'r'" + when "VIEW" + "'v','m'" + end + scope = {} + scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + + def extract_schema_qualified_name(string) + name = Utils.extract_schema_qualified_name(string.to_s) + [name.schema, name.identifier] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index bcef8ac715..f57179ae59 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,8 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + undef to_yaml if method_defined?(:to_yaml) + attr_reader :oid, :fmod, :array def initialize(type_metadata, oid: nil, fmod: nil) @@ -8,7 +10,7 @@ module ActiveRecord @type_metadata = type_metadata @oid = oid @fmod = fmod - @array = /\[\]$/ === type_metadata.sql_type + @array = /\[\]$/.match?(type_metadata.sql_type) end def sql_type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index 9a0b80d7d3..aa7940188a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -19,9 +19,9 @@ module ActiveRecord def quoted if schema - PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier) + PG::Connection.quote_ident(schema) << SEPARATOR << PG::Connection.quote_ident(identifier) else - PGconn.quote_ident(identifier) + PG::Connection.quote_ident(identifier) end end @@ -35,6 +35,12 @@ module ActiveRecord end protected + + def parts + @parts ||= [@schema, @identifier].compact + end + + private def unquote(part) if part && part.start_with?('"') part[1..-2] @@ -42,10 +48,6 @@ module ActiveRecord part end end - - def parts - @parts ||= [@schema, @identifier].compact - end end module Utils # :nodoc: @@ -53,7 +55,7 @@ module ActiveRecord # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt> # extracted from +string+. - # +schema+ is nil if not specified in +string+. + # +schema+ is +nil+ if not specified in +string+. # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+) # +string+ supports the range of schema/table references understood by PostgreSQL, for example: # diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 8001c0dd53..f74f966fd9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -3,18 +3,19 @@ gem "pg", "~> 0.18" require "pg" require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" require "active_record/connection_adapters/postgresql/explain_pretty_printer" require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_creation" require "active_record/connection_adapters/postgresql/schema_definitions" require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" -require "active_record/connection_adapters/statement_pool" module ActiveRecord module ConnectionHandling # :nodoc: @@ -28,11 +29,11 @@ module ActiveRecord conn_params[:user] = conn_params.delete(:username) if conn_params[:username] conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] - # Forward only valid config params to PGconn.connect. - valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + # Forward only valid config params to PG::Connection.connect. + valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] conn_params.slice!(*valid_conn_param_keys) - # The postgres drivers don't allow the creation of an unconnected PGconn object, + # The postgres drivers don't allow the creation of an unconnected PG::Connection object, # so just pass a nil connection object for the time being. ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) end @@ -70,7 +71,7 @@ module ActiveRecord ADAPTER_NAME = "PostgreSQL".freeze NATIVE_DATABASE_TYPES = { - primary_key: "serial primary key", + primary_key: "bigserial primary key", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -108,6 +109,8 @@ module ActiveRecord bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, + interval: { name: "interval" }, + oid: { name: "oid" }, } OID = PostgreSQL::OID #:nodoc: @@ -118,20 +121,6 @@ module ActiveRecord include PostgreSQL::DatabaseStatements include PostgreSQL::ColumnDumper - def schema_creation # :nodoc: - PostgreSQL::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::PostgreSQL.new(self) - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - def supports_index_sort_order? true end @@ -198,8 +187,8 @@ module ActiveRecord end def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError + @connection.status == PG::CONNECTION_OK + rescue PG::Error false end end @@ -212,7 +201,7 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil - @table_alias_length = nil + @max_identifier_length = nil connect add_pg_encoders @@ -233,7 +222,9 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.clear + @lock.synchronize do + @statements.clear + end end def truncate(table_name, name = nil) @@ -242,50 +233,48 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query "SELECT 1" + @lock.synchronize do + @connection.query "SELECT 1" + end true - rescue PGError + rescue PG::Error false end # Close then reopen the connection. def reconnect! - super - @connection.reset - configure_connection + @lock.synchronize do + super + @connection.reset + configure_connection + end end def reset! - clear_cache! - reset_transaction - unless @connection.transaction_status == ::PG::PQTRANS_IDLE - @connection.query "ROLLBACK" + @lock.synchronize do + clear_cache! + reset_transaction + unless @connection.transaction_status == ::PG::PQTRANS_IDLE + @connection.query "ROLLBACK" + end + @connection.query "DISCARD ALL" + configure_connection end - @connection.query "DISCARD ALL" - configure_connection end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - super - @connection.close rescue nil + @lock.synchronize do + super + @connection.close rescue nil + end end def native_database_types #:nodoc: NATIVE_DATABASE_TYPES end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - - # Does PostgreSQL support finding primary key on non-Active Record tables? - def supports_primary_key? #:nodoc: - true - end - def set_standard_conforming_strings execute("SET standard_conforming_strings = on", "SCHEMA") end @@ -306,8 +295,8 @@ module ActiveRecord true end - # Range datatypes weren't introduced until PostgreSQL 9.2 def supports_ranges? + # Range datatypes weren't introduced until PostgreSQL 9.2 postgresql_version >= 90200 end @@ -315,6 +304,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_pgcrypto_uuid? + postgresql_version >= 90400 + end + def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") @@ -359,8 +352,9 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query("SHOW max_identifier_length", "SCHEMA")[0][0].to_i + @max_identifier_length ||= select_value("SHOW max_identifier_length", "SCHEMA").to_i end + alias index_name_length table_alias_length # Set the authorized user for this session def session_auth=(user) @@ -372,19 +366,10 @@ module ActiveRecord @use_insert_returning end - def valid_type?(type) - !native_database_types[type].nil? - end - def update_table_definition(table_name, base) #:nodoc: PostgreSQL::Table.new(table_name, base) end - def lookup_cast_type(sql_type) # :nodoc: - oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first["oid"].to_i - super(oid) - end - def column_name_for_operation(operation, node) # :nodoc: OPERATION_ALIASES.fetch(operation) { operation.downcase } end @@ -400,10 +385,16 @@ module ActiveRecord @connection.server_version end - protected + def default_index_type?(index) # :nodoc: + index.using == :btree || super + end + + private # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" + NUMERIC_VALUE_OUT_OF_RANGE = "22003" + NOT_NULL_VIOLATION = "23502" FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" SERIALIZATION_FAILURE = "40001" @@ -412,13 +403,17 @@ module ActiveRecord def translate_exception(exception, message) return exception unless exception.respond_to?(:result) - case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) + case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message) when VALUE_LIMIT_VIOLATION ValueTooLong.new(message) + when NUMERIC_VALUE_OUT_OF_RANGE + RangeError.new(message) + when NOT_NULL_VIOLATION + NotNullViolation.new(message) when SERIALIZATION_FAILURE SerializationFailure.new(message) when DEADLOCK_DETECTED @@ -428,9 +423,7 @@ module ActiveRecord end end - private - - def get_oid_type(oid, fmod, column_name, sql_type = "") # :nodoc: + def get_oid_type(oid, fmod, column_name, sql_type = "".freeze) if !type_map.key?(oid) load_additional_types(type_map, [oid]) end @@ -443,11 +436,11 @@ module ActiveRecord } end - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) register_class_with_limit m, "int2", Type::Integer register_class_with_limit m, "int4", Type::Integer register_class_with_limit m, "int8", Type::Integer - m.alias_type "oid", "int2" + m.register_type "oid", OID::Oid.new m.register_type "float4", Type::Float.new m.alias_type "float8", "float4" m.register_type "text", Type::Text.new @@ -482,8 +475,10 @@ module ActiveRecord m.register_type "polygon", OID::SpecializedString.new(:polygon) m.register_type "circle", OID::SpecializedString.new(:circle) - # FIXME: why are we keeping these types as strings? - m.alias_type "interval", "varchar" + m.register_type "interval" do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::SpecializedString.new(:interval, precision: precision) + end register_class_with_precision m, "time", Type::Time register_class_with_precision m, "timestamp", OID::DateTime @@ -511,7 +506,7 @@ module ActiveRecord load_additional_types(m) end - def extract_limit(sql_type) # :nodoc: + def extract_limit(sql_type) case sql_type when /^bigint/i, /^int8/i 8 @@ -523,7 +518,7 @@ module ActiveRecord end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(default) # :nodoc: + def extract_value_from_default(default) case default # Quoted types when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m @@ -549,15 +544,15 @@ module ActiveRecord end end - def extract_default_function(default_value, default) # :nodoc: + def extract_default_function(default_value, default) default if has_default_function?(default_value, default) end - def has_default_function?(default_value, default) # :nodoc: - !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) + def has_default_function?(default_value, default) + !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default) end - def load_additional_types(type_map, oids = nil) # :nodoc: + def load_additional_types(type_map, oids = nil) initializer = OID::TypeMapInitializer.new(type_map) if supports_ranges? @@ -601,7 +596,11 @@ module ActiveRecord def exec_no_cache(sql, name, binds) type_casted_binds = type_casted_binds(binds) - log(sql, name, binds, type_casted_binds) { @connection.async_exec(sql, type_casted_binds) } + log(sql, name, binds, type_casted_binds) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.async_exec(sql, type_casted_binds) + end + end end def exec_cache(sql, name, binds) @@ -609,7 +608,9 @@ module ActiveRecord type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds, stmt_key) do - @connection.exec_prepared(stmt_key, type_casted_binds) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.exec_prepared(stmt_key, type_casted_binds) + end end rescue ActiveRecord::StatementInvalid => e raise unless is_cached_plan_failure?(e) @@ -619,8 +620,10 @@ module ActiveRecord if in_transaction? raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message) else - # outside of transactions we can simply flush this query and retry - @statements.delete sql_key(sql) + @lock.synchronize do + # outside of transactions we can simply flush this query and retry + @statements.delete sql_key(sql) + end retry end end @@ -637,7 +640,7 @@ module ActiveRecord CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze def is_cached_plan_failure?(e) pgerror = e.cause - code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC) rescue false @@ -656,25 +659,27 @@ module ActiveRecord # Prepare the statement if it hasn't been prepared, return # the statement key. def prepare_statement(sql) - sql_key = sql_key(sql) - unless @statements.key? sql_key - nextkey = @statements.next_key - begin - @connection.prepare nextkey, sql - rescue => e - raise translate_exception_class(e, sql) + @lock.synchronize do + sql_key = sql_key(sql) + unless @statements.key? sql_key + nextkey = @statements.next_key + begin + @connection.prepare nextkey, sql + rescue => e + raise translate_exception_class(e, sql) + end + # Clear the queue + @connection.get_last_result + @statements[sql_key] = nextkey end - # Clear the queue - @connection.get_last_result - @statements[sql_key] = nextkey + @statements[sql_key] end - @statements[sql_key] end # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect - @connection = PGconn.connect(@connection_parameters) + @connection = PG.connect(@connection_parameters) configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") @@ -719,7 +724,7 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id_result(sequence_name) # :nodoc: + def last_insert_id_result(sequence_name) exec_query("SELECT currval('#{sequence_name}')", "SQL") end @@ -741,28 +746,28 @@ module ActiveRecord # Query implementation notes: # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name - def column_definitions(table_name) # :nodoc: + def column_definitions(table_name) query(<<-end_sql, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, - (SELECT c.collname FROM pg_collation c, pg_type t - WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation), - col_description(a.attrelid, a.attnum) AS comment - FROM pg_attribute a LEFT JOIN pg_attrdef d - ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass + c.collname, col_description(a.attrelid, a.attnum) AS comment + FROM pg_attribute a + LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation + WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum end_sql end - def extract_table_ref_from_insert_sql(sql) # :nodoc: + def extract_table_ref_from_insert_sql(sql) sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end - def create_table_definition(*args) # :nodoc: - PostgreSQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::PostgreSQL.new(self) end def can_perform_case_insensitive_comparison_for?(column) @@ -771,10 +776,14 @@ module ActiveRecord sql = <<-end_sql SELECT exists( SELECT * FROM pg_proc + WHERE proname = 'lower' + AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector + ) OR exists( + SELECT * FROM pg_proc INNER JOIN pg_cast - ON casttarget::text::oidvector = proargtypes + ON ARRAY[casttarget]::oidvector = proargtypes WHERE proname = 'lower' - AND castsource = '#{column.sql_type}'::regtype::oid + AND castsource = #{quote column.sql_type}::regtype ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| @@ -788,7 +797,6 @@ module ActiveRecord map[Integer] = PG::TextEncoder::Integer.new map[TrueClass] = PG::TextEncoder::Boolean.new map[FalseClass] = PG::TextEncoder::Boolean.new - map[Float] = PG::TextEncoder::Float.new @connection.type_map_for_queries = map end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 8219f132c3..4d339b0a8c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -21,6 +21,22 @@ module ActiveRecord @data_sources = @data_sources.dup end + def encode_with(coder) + coder["columns"] = @columns + coder["columns_hash"] = @columns_hash + coder["primary_keys"] = @primary_keys + coder["data_sources"] = @data_sources + coder["version"] = ActiveRecord::Migrator.current_version + end + + def init_with(coder) + @columns = coder["columns"] + @columns_hash = coder["columns_hash"] + @primary_keys = coder["primary_keys"] + @data_sources = coder["data_sources"] + @version = coder["version"] + end + def primary_keys(table_name) @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil end @@ -32,8 +48,6 @@ module ActiveRecord @data_sources[name] = connection.data_source_exists?(name) end - alias table_exists? data_source_exists? - deprecate table_exists?: "use #data_source_exists? instead" # Add internal cache for table with +table_name+. def add(table_name) @@ -47,8 +61,6 @@ module ActiveRecord def data_sources(name) @data_sources[name] end - alias tables data_sources - deprecate tables: "use #data_sources instead" # Get the columns for a table def columns(table_name) @@ -83,8 +95,6 @@ module ActiveRecord @primary_keys.delete name @data_sources.delete name end - alias clear_table_cache! clear_data_source_cache! - deprecate clear_table_cache!: "use #clear_data_source_cache! instead" def marshal_dump # if we get current version during initialization, it happens stack over flow. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index f01ed67b0f..7276a65098 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -18,15 +18,11 @@ module ActiveRecord quoted_date(value) end - private + def quoted_binary(value) + "x'#{value.hex}'" + end - def _quote(value) - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end + private def _type_cast(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb index 70c0d28830..bc798d1dbb 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -1,15 +1,8 @@ module ActiveRecord module ConnectionAdapters module SQLite3 - class SchemaCreation < AbstractAdapter::SchemaCreation + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private - - def column_options(o) - options = super - options[:null] = false if o.primary_key - options - end - def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb new file mode 100644 index 0000000000..e157e4b218 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + if %i(integer bigint).include?(type) && (options.delete(:auto_increment) == true || !options.key?(:default)) + type = :primary_key + end + + super + end + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def references(*args, **options) + super(*args, type: :integer, **options) + end + alias :belongs_to :references + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb new file mode 100644 index 0000000000..eec018eda3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnDumper # :nodoc: + private + + def default_primary_key?(column) + schema_type(column) == :integer + end + + def explicit_primary_key_default?(column) + column.bigint? + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb new file mode 100644 index 0000000000..e02491edb6 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -0,0 +1,92 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + index_sql = select_value(<<-SQL, "SCHEMA") + SELECT sql + FROM sqlite_master + WHERE name = #{quote(row['name'])} AND type = 'index' + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE name = #{quote(row['name'])} AND type = 'index' + SQL + + /\sWHERE\s+(?<where>.+)$/i =~ index_sql + + columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col| + col["name"] + end + + IndexDefinition.new( + table_name, + row["name"], + row["unique"] != 0, + columns, + where: where + ) + end + end + + private + def schema_creation + SQLite3::SchemaCreation.new(self) + end + + def create_table_definition(*args) + SQLite3::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + default = \ + case field["dflt_value"] + when /^null$/i + nil + when /^'(.*)'$/m + $1.gsub("''", "'") + when /^"(.*)"$/m + $1.gsub('""', '"') + else + field["dflt_value"] + end + + type_metadata = fetch_type_metadata(field["type"]) + Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"]) + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'table','view'" + + sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" + sql << " AND name = #{scope[:name]}" if scope[:name] + sql << " AND type IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + type = \ + case type + when "BASE TABLE" + "'table'" + when "VIEW" + "'view'" + end + scope = {} + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e2b534b511..7233325d5a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -3,6 +3,9 @@ require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/sqlite3/explain_pretty_printer" require "active_record/connection_adapters/sqlite3/quoting" require "active_record/connection_adapters/sqlite3/schema_creation" +require "active_record/connection_adapters/sqlite3/schema_definitions" +require "active_record/connection_adapters/sqlite3/schema_dumper" +require "active_record/connection_adapters/sqlite3/schema_statements" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -52,6 +55,8 @@ module ActiveRecord ADAPTER_NAME = "SQLite".freeze include SQLite3::Quoting + include SQLite3::ColumnDumper + include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", @@ -75,12 +80,8 @@ module ActiveRecord end end - def schema_creation # :nodoc: - SQLite3::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::SQLite.new(self) + def update_table_definition(table_name, base) # :nodoc: + SQLite3::Table.new(table_name, base) end def initialize(connection, logger, connection_options, config) @@ -88,6 +89,8 @@ module ActiveRecord @active = nil @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) + + configure_connection end def supports_ddl_transactions? @@ -102,23 +105,12 @@ module ActiveRecord sqlite_version >= "3.8.0" end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - def supports_primary_key? #:nodoc: + def requires_reloading? true end - def requires_reloading? - true + def supports_foreign_keys_in_create? + sqlite_version >= "3.6.19" end def supports_views? @@ -154,10 +146,6 @@ module ActiveRecord true end - def valid_type?(type) - true - end - # Returns 62. SQLite supports index names up to 64 # characters. The rest is used by Rails internally to perform # temporary rename operations @@ -178,6 +166,19 @@ module ActiveRecord true end + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity # :nodoc: + old = select_value("PRAGMA foreign_keys") + + begin + execute("PRAGMA foreign_keys = OFF") + yield + ensure + execute("PRAGMA foreign_keys = #{old}") + end + end + #-- # DATABASE STATEMENTS ====================================== #++ @@ -191,30 +192,32 @@ module ActiveRecord type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do - # Don't cache statements if they are not prepared - unless prepare - stmt = @connection.prepare(sql) - begin - cols = stmt.columns - unless without_prepared_statement?(binds) - stmt.bind_params(type_casted_binds) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + # Don't cache statements if they are not prepared + unless prepare + stmt = @connection.prepare(sql) + begin + cols = stmt.columns + unless without_prepared_statement?(binds) + stmt.bind_params(type_casted_binds) + end + records = stmt.to_a + ensure + stmt.close end + else + cache = @statements[sql] ||= { + stmt: @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params(type_casted_binds) records = stmt.to_a - ensure - stmt.close end - else - cache = @statements[sql] ||= { - stmt: @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params(type_casted_binds) - records = stmt.to_a - end - ActiveRecord::Result.new(cols, records) + ActiveRecord::Result.new(cols, records) + end end end @@ -229,120 +232,27 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.execute(sql) + end + end end def begin_db_transaction #:nodoc: - log("begin transaction",nil) { @connection.transaction } + log("begin transaction", nil) { @connection.transaction } end def commit_db_transaction #:nodoc: - log("commit transaction",nil) { @connection.commit } + log("commit transaction", nil) { @connection.commit } end def exec_rollback_db_transaction #:nodoc: - log("rollback transaction",nil) { @connection.rollback } + log("rollback transaction", nil) { @connection.rollback } end # SCHEMA STATEMENTS ======================================== - def tables(name = nil) # :nodoc: - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #tables currently returns both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only return tables. - Use #data_sources instead. - MSG - - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end - - data_sources - end - - def data_sources - select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def table_exists?(table_name) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #table_exists? currently checks both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only check tables. - Use #data_source_exists? instead. - MSG - - data_source_exists?(table_name) - end - - def data_source_exists?(table_name) - return false unless table_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(table_name)}" - - select_values(sql, "SCHEMA").any? - end - - def views # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(view_name)}" - - select_values(sql, "SCHEMA").any? - end - - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/m - field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/m - field["dflt_value"] = $1.gsub('""', '"') - end - - collation = field["collation"] - sql_type = field["type"] - type_metadata = fetch_type_metadata(sql_type) - new_column(field["name"], field["dflt_value"], type_metadata, field["notnull"].to_i == 0, table_name, nil, collation) - end - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| - sql = <<-SQL - SELECT sql - FROM sqlite_master - WHERE name=#{quote(row['name'])} AND type='index' - UNION ALL - SELECT sql - FROM sqlite_temp_master - WHERE name=#{quote(row['name'])} AND type='index' - SQL - index_sql = exec_query(sql).first["sql"] - match = /\sWHERE\s+(.+)$/i.match(index_sql) - where = match[1] if match - IndexDefinition.new( - table_name, - row["name"], - row["unique"] != 0, - exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| - col["name"] - }, nil, nil, where) - end - end - def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f["pk"] > 0 } pks.sort_by { |f| f["pk"] }.map { |f| f["name"] } @@ -403,14 +313,13 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: alter_table(table_name) do |definition| - include_default = options_include_default?(options) definition[column_name].instance_eval do self.type = type self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default + self.default = options[:default] if options.include?(:default) self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) + self.scale = options[:scale] if options.include?(:scale) self.collation = options[:collation] if options.include?(:collation) end end @@ -422,15 +331,34 @@ module ActiveRecord rename_column_indexes(table_name, column.name, new_column_name) end - protected + def add_reference(table_name, ref_name, **options) # :nodoc: + super(table_name, ref_name, type: :integer, **options) + end + alias :add_belongs_to :add_reference + + def foreign_keys(table_name) + fk_info = select_all("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") + fk_info.map do |row| + options = { + column: row["from"], + primary_key: row["to"], + on_delete: extract_foreign_key_action(row["on_delete"]), + on_update: extract_foreign_key_action(row["on_update"]) + } + ForeignKeyDefinition.new(table_name, row["table"], options) + end + end + + private def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? table_structure_with_collation(table_name, structure) end + alias column_definitions table_structure - def alter_table(table_name, options = {}) #:nodoc: + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" caller = lambda { |definition| yield definition if block_given? } @@ -441,12 +369,12 @@ module ActiveRecord end end - def move_table(from, to, options = {}, &block) #:nodoc: + def move_table(from, to, options = {}, &block) copy_table(from, to, options, &block) drop_table(from) end - def copy_table(from, to, options = {}) #:nodoc: + def copy_table(from, to, options = {}) from_primary_key = primary_key(from) options[:id] = false create_table(to, options) do |definition| @@ -472,7 +400,7 @@ module ActiveRecord options[:rename] || {}) end - def copy_table_indexes(from, to, rename = {}) #:nodoc: + def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name if to == "a#{from}" @@ -495,7 +423,7 @@ module ActiveRecord end end - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + def copy_table_contents(from, to, columns, rename = {}) column_mappings = Hash[columns.map { |name| [name, name] }] rename.each { |a| column_mappings[a.last] = a.first } from_columns = columns(from).collect(&:name) @@ -509,7 +437,7 @@ module ActiveRecord end def sqlite_version - @sqlite_version ||= SQLite3Adapter::Version.new(select_value("select sqlite_version(*)")) + @sqlite_version ||= SQLite3Adapter::Version.new(select_value("SELECT sqlite_version(*)")) end def translate_exception(exception, message) @@ -520,20 +448,25 @@ module ActiveRecord # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ RecordNotUnique.new(message) + when /.* may not be NULL/, /NOT NULL constraint failed: .*/ + NotNullViolation.new(message) + when /FOREIGN KEY constraint failed/i + InvalidForeignKey.new(message) else super end end - private COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze def table_structure_with_collation(table_name, basic_structure) collation_hash = {} - sql = "SELECT sql FROM - (SELECT * FROM sqlite_master UNION ALL - SELECT * FROM sqlite_temp_master) - WHERE type='table' and name='#{ table_name }' \;" + sql = <<-SQL + SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type = 'table' AND name = #{quote(table_name)} + SQL # Result will have following sample string # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -564,6 +497,14 @@ module ActiveRecord basic_structure.to_hash end end + + def arel_visitor + Arel::Visitors::SQLite.new(self) + end + + def configure_connection + execute("PRAGMA foreign_keys = ON", "SCHEMA") + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 273b1b0b5c..790db56185 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -6,7 +6,7 @@ module ActiveRecord DEFAULT_STATEMENT_LIMIT = 1000 def initialize(statement_limit = nil) - @cache = Hash.new { |h,pid| h[pid] = {} } + @cache = Hash.new { |h, pid| h[pid] = {} } @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 2ede92feff..b8fbb489b6 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 622df0cfc1..8f78330d4a 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -171,41 +171,37 @@ module ActiveRecord return super if block_given? || primary_key.nil? || scope_attributes? || - columns_hash.include?(inheritance_column) || - ids.first.kind_of?(Array) - - id = ids.first - if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) - You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id`. - MSG - end + columns_hash.include?(inheritance_column) + + id = ids.first + + return super if id.kind_of?(Array) || + id.is_a?(ActiveRecord::Base) key = primary_key statement = cached_find_by_statement(key) { |params| where(key => params.bind).limit(1) } + record = statement.execute([id], self, connection).first unless record raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id) end record - rescue RangeError + rescue ::RangeError raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", name, primary_key) end def find_by(*args) # :nodoc: - return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any? + return super if scope_attributes? || reflect_on_all_aggregations.any? hash = args.first - return super if hash.values.any? { |v| - v.nil? || Array === v || Hash === v || Relation === v + return super if !(Hash === hash) || hash.values.any? { |v| + v.nil? || Array === v || Hash === v || Relation === v || Base === v } # We can't cache Post.find_by(author: david) ...yet @@ -223,7 +219,7 @@ module ActiveRecord statement.execute(hash.values, self, connection).first rescue TypeError raise ActiveRecord::StatementInvalid - rescue RangeError + rescue ::RangeError nil end end @@ -239,7 +235,9 @@ module ActiveRecord def generated_association_methods @generated_association_methods ||= begin mod = const_set(:GeneratedAssociationMethods, Module.new) + private_constant :GeneratedAssociationMethods include mod + mod end end @@ -299,24 +297,24 @@ module ActiveRecord private - def cached_find_by_statement(key, &block) # :nodoc: + def cached_find_by_statement(key, &block) cache = @find_by_statement_cache[connection.prepared_statements] cache[key] || cache.synchronize { cache[key] ||= StatementCache.create(connection, &block) } end - def relation # :nodoc: + def relation relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? && !ignore_default_scope? - relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) else relation end end - def table_metadata # :nodoc: + def table_metadata TableMetadata.new(self, arel_table) end end @@ -330,8 +328,8 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil) - @attributes = self.class._default_attributes.deep_dup self.class.define_attribute_methods + @attributes = self.class._default_attributes.deep_dup init_internals initialize_internals_callback @@ -452,7 +450,7 @@ module ActiveRecord # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash if id - self.class.hash ^ self.id.hash + self.class.hash ^ id.hash else super end @@ -474,7 +472,7 @@ module ActiveRecord # Allows sort on objects def <=>(other_object) if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key + to_key <=> other_object.to_key else super end @@ -538,7 +536,7 @@ module ActiveRecord # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) - Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access + Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access end private @@ -561,7 +559,6 @@ module ActiveRecord @marked_for_destruction = false @destroyed_by_association = nil @new_record = true - @txn = nil @_start_transaction_state = {} @transaction_state = nil end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index e2da512813..cbd71a3779 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -12,13 +12,21 @@ module ActiveRecord # # * +id+ - The id of the object you wish to reset a counter on. # * +counters+ - One or more association counters to reset. Association name or counter name can be given. + # * <tt>:touch</tt> - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # - # # For Post with id #1 records reset the comments_count + # # For the Post with id #1, reset the comments_count # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) + # + # # Like above, but also touch the +updated_at+ and/or +updated_on+ + # # attributes. + # Post.reset_counters(1, :comments, touch: true) + def reset_counters(id, *counters, touch: nil) object = find(id) + counters.each do |counter_association| has_many_association = _reflect_on_association(counter_association) unless has_many_association @@ -37,10 +45,12 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - unscoped.where(primary_key => object.id).update_all( - counter_name => object.send(counter_association).count(:all) - ) + updates = { counter_name.to_sym => object.send(counter_association).count(:all) } + updates.merge!(touch_updates(touch)) if touch + + unscoped.where(primary_key => object.id).update_all(updates) end + return true end @@ -55,6 +65,9 @@ module ActiveRecord # * +id+ - The id of the object you wish to update a counter on or an array of ids. # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. + # * <tt>:touch</tt> option - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # @@ -73,13 +86,29 @@ module ActiveRecord # # UPDATE posts # # SET comment_count = COALESCE(comment_count, 0) + 1 # # WHERE id IN (10, 15) + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # # and update the updated_at value for each counter. + # Post.update_counters [10, 15], comment_count: 1, touch: true + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) + 1, + # # `updated_at` = '2016-10-13T09:59:23-05:00' + # # WHERE id IN (10, 15) def update_counters(id, counters) + touch = counters.delete(:touch) + updates = counters.map do |counter_name, value| operator = value < 0 ? "-" : "+" quoted_column = connection.quote_column_name(counter_name) "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" end + if touch + touch_updates = touch_updates(touch) + updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? + end + unscoped.where(primary_key => id).update_all updates.join(", ") end @@ -94,13 +123,20 @@ module ActiveRecord # # * +counter_name+ - The name of the field that should be incremented. # * +id+ - The id of the object that should be incremented or an array of ids. + # * <tt>:touch</tt> - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # # # Increment the posts_count column for the record with an id of 5 # DiscussionBoard.increment_counter(:posts_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) + # + # # Increment the posts_count column for the record with an id of 5 + # # and update the updated_at value. + # DiscussionBoard.increment_counter(:posts_count, 5, touch: true) + def increment_counter(counter_name, id, touch: nil) + update_counters(id, counter_name => 1, touch: touch) end # Decrement a numeric field by one, via a direct SQL update. @@ -112,14 +148,28 @@ module ActiveRecord # # * +counter_name+ - The name of the field that should be decremented. # * +id+ - The id of the object that should be decremented or an array of ids. + # * <tt>:touch</tt> - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # # # Decrement the posts_count column for the record with an id of 5 # DiscussionBoard.decrement_counter(:posts_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) + # + # # Decrement the posts_count column for the record with an id of 5 + # # and update the updated_at value. + # DiscussionBoard.decrement_counter(:posts_count, 5, touch: true) + def decrement_counter(counter_name, id, touch: nil) + update_counters(id, counter_name => -1, touch: touch) end + + private + def touch_updates(touch) + touch = timestamp_attributes_for_update_in_model if touch == true + touch_time = current_time_from_proper_timezone + Array(touch).map { |column| [ column, touch_time ] }.to_h + end end private diff --git a/activerecord/lib/active_record/define_callbacks.rb b/activerecord/lib/active_record/define_callbacks.rb new file mode 100644 index 0000000000..7d955a24be --- /dev/null +++ b/activerecord/lib/active_record/define_callbacks.rb @@ -0,0 +1,20 @@ +module ActiveRecord + # This module exists because `ActiveRecord::AttributeMethods::Dirty` needs to + # define callbacks, but continue to have its version of `save` be the super + # method of `ActiveRecord::Callbacks`. This will be removed when the removal + # of deprecated code removes this need. + module DefineCallbacks + extend ActiveSupport::Concern + + module ClassMethods # :nodoc: + include ActiveModel::Callbacks + end + + included do + include ActiveModel::Validations::Callbacks + + define_model_callbacks :initialize, :find, :touch, only: :after + define_model_callbacks :save, :create, :update, :destroy + end + end +end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 9a7a8d25bb..3a9625092e 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,17 +1,14 @@ -require "active_support/core_ext/regexp" - module ActiveRecord module DynamicMatchers #:nodoc: - def respond_to_missing?(name, include_private = false) - if self == Base - super - else - match = Method.match(self, name) - match && match.valid? || super - end - end - private + def respond_to_missing?(name, _) + if self == Base + super + else + match = Method.match(self, name) + match && match.valid? || super + end + end def method_missing(name, *arguments, &block) match = Method.match(self, name) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 0a94ab58dd..12ef58a941 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -95,8 +95,7 @@ module ActiveRecord module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums, instance_writer: false) - base.defined_enums = {} + base.class_attribute(:defined_enums, instance_writer: false, default: {}) end def inherited(base) # :nodoc: @@ -140,6 +139,8 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :mapping, :subtype @@ -152,11 +153,12 @@ module ActiveRecord definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new - name = name.to_sym + name = name.to_s # def self.statuses() statuses end - detect_enum_conflict!(name, name.to_s.pluralize, true) - klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + detect_enum_conflict!(name, name.pluralize, true) + singleton_class.send(:define_method, name.pluralize) { enum_values } + defined_enums[name] = enum_values detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") @@ -168,7 +170,7 @@ module ActiveRecord _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index - pairs.each do |value, i| + pairs.each do |label, value| if enum_prefix == true prefix = "#{name}_" elsif enum_prefix @@ -180,23 +182,23 @@ module ActiveRecord suffix = "_#{enum_suffix}" end - value_method_name = "#{prefix}#{value}#{suffix}" - enum_values[value] = i + value_method_name = "#{prefix}#{label}#{suffix}" + enum_values[label] = value + label = label.to_s - # def active?() status == 0 end + # def active?() status == "active" end klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[attr] == value.to_s } + define_method("#{value_method_name}?") { self[attr] == label } - # def active!() update! status: :active end + # def active!() update!(status: 0) end klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") define_method("#{value_method_name}!") { update!(attr => value) } - # scope :active, -> { where status: 0 } + # scope :active, -> { where(status: 0) } klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } end end - defined_enums[name.to_s] = enum_values end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 8fbe43e3ec..18fac5af1b 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -43,7 +43,7 @@ module ActiveRecord # Raised when connection to the database could not been established (for example when # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection] - # is given a nil object). + # is given a +nil+ object). class ConnectionNotEstablished < ActiveRecordError end @@ -95,19 +95,9 @@ module ActiveRecord # # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - def initialize(message = nil, original_exception = nil) - if original_exception - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize(message = nil) super(message || $!.try(:message)) end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end # Defunct wrapper class kept for compatibility. @@ -123,10 +113,46 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end + # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type. + class MismatchedForeignKey < StatementInvalid + def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) + @adapter = adapter + if table + msg = <<-EOM.strip_heredoc + Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`. + This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`. + To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`). + EOM + else + msg = <<-EOM + There is a mismatch between the foreign key and primary key column types. + Verify that the foreign key column type and the primary key of the associated table match types. + EOM + end + if message + msg << "\nOriginal message: #{message}" + end + super(msg) + end + + private + def column_type(table, column) + @adapter.columns(table).detect { |c| c.name == column }.sql_type + end + end + + # Raised when a record cannot be inserted or updated because it would violate a not null constraint. + class NotNullViolation < StatementInvalid + end + # Raised when a record cannot be inserted or updated because a value too long for a column type. class ValueTooLong < StatementInvalid end + # Raised when values that executed are out of range. + class RangeError < StatementInvalid + end + # Raised when number of bind variables in statement given to +:condition+ key # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) # does not match number of expected values supplied. diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 980b8e1baa..8f7ae2c33c 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,4 +1,3 @@ -require "active_support/lazy_load_hooks" require "active_record/explain_registry" module ActiveRecord diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 706b57842f..abd8cfc8f2 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -18,10 +18,13 @@ module ActiveRecord # # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. - IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i def ignore_payload?(payload) - payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS + payload[:exception] || + payload[:cached] || + IGNORED_PAYLOADS.include?(payload[:name]) || + payload[:sql] !~ EXPLAINED_SQLS end ActiveSupport::Notifications.subscribe("sql.active_record", new) diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 5ba354d758..6cf2e01179 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -66,10 +66,13 @@ module ActiveRecord # Validate our unmarshalled data. def validate(data) unless Hash === data || YAML::Omap === data - raise Fixture::FormatError, "fixture is not a hash" + raise Fixture::FormatError, "fixture is not a hash: #{@file}" end - raise Fixture::FormatError unless data.all? { |name, row| Hash === row } + invalid = data.reject { |_, row| Hash === row } + if invalid.any? + raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}" + end data end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 8b47fbdbe4..bad6542be2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -70,13 +70,32 @@ module ActiveRecord # test. To ensure consistent data, the environment deletes the fixtures before running the load. # # In addition to being available in the database, the fixture's data may also be accessed by - # using a special dynamic method, which has the same name as the model, and accepts the - # name of the fixture to instantiate: + # using a special dynamic method, which has the same name as the model. # - # test "find" do + # Passing in a fixture name to this dynamic method returns the fixture matching this name: + # + # test "find one" do # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name # end # + # Passing in multiple fixture names returns all fixtures matching these names: + # + # test "find all by name" do + # assert_equal 2, web_sites(:rubyonrails, :google).length + # end + # + # Passing in no arguments returns all fixtures: + # + # test "find all" do + # assert_equal 2, web_sites.length + # end + # + # Passing in any fixture name that does not exist will raise <tt>StandardError</tt>: + # + # test "find by name that does not exist" do + # assert_raise(StandardError) { web_sites(:reddit) } + # end + # # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the # following tests: # @@ -88,7 +107,7 @@ module ActiveRecord # assert_equal "Ruby on Rails", @rubyonrails.name # end # - # In order to use these methods to access fixtured data within your testcases, you must specify one of the + # In order to use these methods to access fixtured data within your test cases, you must specify one of the # following in your ActiveSupport::TestCase-derived class: # # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) @@ -103,7 +122,7 @@ module ActiveRecord # # = Dynamic fixtures with ERB # - # Some times you don't care about the content of the fixtures as much as you care about the volume. + # Sometimes you don't care about the content of the fixtures as much as you care about the volume. # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load # testing, like: # @@ -415,9 +434,9 @@ module ActiveRecord # possibly in a folder with the same name. #++ - MAX_ID = 2 ** 30 - 1 + MAX_ID = 2**30 - 1 - @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } + @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} } def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: config.pluralize_table_names ? @@ -536,16 +555,16 @@ module ActiveRecord update_all_loaded_fixtures fixtures_map connection.transaction(requires_new: true) do - deleted_tables = Set.new + 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 table_rows.each_key do |table| - unless deleted_tables.include? table + unless deleted_tables[conn].include? table conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" end - deleted_tables << table + deleted_tables[conn] << table end table_rows.each do |fixture_set_name, rows| @@ -597,18 +616,18 @@ module ActiveRecord @fixtures = read_fixture_files(path) - @connection = connection + @connection = connection - @table_name = ( model_class.respond_to?(:table_name) ? + @table_name = (model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(name, config) ) + self.class.default_fixture_table_name(name, config)) end def [](x) fixtures[x] end - def []=(k,v) + def []=(k, v) fixtures[k] = v end @@ -629,7 +648,7 @@ module ActiveRecord fixtures.delete("DEFAULTS") # track any join tables we need to insert later - rows = Hash.new { |h,table| h[table] = [] } + rows = Hash.new { |h, table| h[table] = [] } rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash @@ -859,32 +878,12 @@ module ActiveRecord included do class_attribute :fixture_path, instance_writer: false - class_attribute :fixture_table_names - class_attribute :fixture_class_names - class_attribute :use_transactional_tests - class_attribute :use_transactional_fixtures - class_attribute :use_instantiated_fixtures # true, false, or :no_instances - class_attribute :pre_loaded_fixtures - class_attribute :config - - singleton_class.deprecate "use_transactional_fixtures=" => "use use_transactional_tests= instead" - - self.fixture_table_names = [] - self.use_instantiated_fixtures = false - self.pre_loaded_fixtures = false - self.config = ActiveRecord::Base - - self.fixture_class_names = {} - - silence_warnings do - define_singleton_method :use_transactional_tests do - if use_transactional_fixtures.nil? - true - else - use_transactional_fixtures - end - end - end + class_attribute :fixture_table_names, default: [] + class_attribute :fixture_class_names, default: {} + class_attribute :use_transactional_tests, default: true + class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances + class_attribute :pre_loaded_fixtures, default: false + class_attribute :config, default: ActiveRecord::Base end module ClassMethods @@ -897,7 +896,7 @@ module ActiveRecord # # The keys must be the fixture names, that coincide with the short paths to the fixture files. def set_fixture_class(class_names = {}) - self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) + self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys) end def fixtures(*fixture_set_names) @@ -921,6 +920,8 @@ module ActiveRecord define_method(accessor_name) do |*fixture_names| force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload + return_single_record = fixture_names.size == 1 + fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty? @fixture_cache[fs_name] ||= {} @@ -935,7 +936,7 @@ module ActiveRecord end end - instances.size == 1 ? instances.first : instances + return_single_record ? instances.first : instances end private accessor_name end @@ -982,6 +983,7 @@ module ActiveRecord @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false + connection.pool.lock_thread = true end # When connections are established in the future, begin a transaction too @@ -997,6 +999,7 @@ module ActiveRecord if connection && !@fixture_connections.include?(connection) connection.begin_transaction joinable: false + connection.pool.lock_thread = true @fixture_connections << connection end end @@ -1019,6 +1022,7 @@ module ActiveRecord ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber @fixture_connections.each do |connection| connection.rollback_transaction if connection.transaction_open? + connection.pool.lock_thread = false end @fixture_connections.clear else diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index f33456a744..1a937dbcf7 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,7 +6,7 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index a1d4f47372..5776807507 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -38,8 +38,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. # This is true, by default. - class_attribute :store_full_sti_class, instance_writer: false - self.store_full_sti_class = true + class_attribute :store_full_sti_class, instance_writer: false, default: true end module ClassMethods @@ -130,16 +129,26 @@ module ActiveRecord store_full_sti_class ? name : name.demodulize end + def inherited(subclass) + subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new) + super + end + protected # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) - if type_name.match(/^::/) + if type_name.start_with?("::".freeze) # If the type is prefixed with a scope operator then we assume that # the type_name is an absolute reference. ActiveSupport::Dependencies.constantize(type_name) else + type_candidate = @_type_candidates_cache[type_name] + if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate) + return type_constant + end + # Build a list of candidates to search for candidates = [] name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } @@ -147,7 +156,10 @@ module ActiveRecord candidates.each do |candidate| constant = ActiveSupport::Dependencies.safe_constantize(candidate) - return constant if candidate == constant.to_s + if candidate == constant.to_s + @_type_candidates_cache[type_name] = candidate + return constant + end end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) @@ -204,7 +216,7 @@ module ActiveRecord def subclass_from_attributes(attrs) attrs = attrs.to_h if attrs.respond_to?(:permitted?) if attrs.is_a?(Hash) - subclass_name = attrs.with_indifferent_access[inheritance_column] + subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym] if subclass_name.present? find_sti_class(subclass_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index e4c7a55541..cf954852bc 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -7,17 +7,24 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp in the cache key. - # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # Indicates the format used to generate the timestamp in the cache key, if + # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # # This is +:usec+, by default. - class_attribute :cache_timestamp_format, instance_writer: false - self.cache_timestamp_format = :usec + class_attribute :cache_timestamp_format, instance_writer: false, default: :usec + + ## + # :singleton-method: + # Indicates whether to use a stable #cache_key method that is accompanied + # by a changing version in the #cache_version method. + # + # This is +false+, by default until Rails 6.0. + class_attribute :cache_versioning, instance_writer: false, default: false end - # Returns a String, which Action Pack uses for constructing a URL to this - # object. The default implementation returns this record's id as a String, - # or nil if this record's unsaved. + # Returns a +String+, which Action Pack uses for constructing a URL to this + # object. The default implementation returns this record's id as a +String+, + # or +nil+ if this record's unsaved. # # For example, suppose that you have a User model, and that you have a # <tt>resources :users</tt> route. Normally, +user_path+ will @@ -42,29 +49,62 @@ module ActiveRecord id && id.to_s # Be sure to stringify the id for routes end - # Returns a cache key that can be used to identify this record. + # Returns a stable cache key that can be used to identify this record. # # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5" # - # You can also pass a list of named timestamps, and the newest in the list will be - # used to generate the key: + # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier, + # the cache key will also include a version. # - # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + # Product.cache_versioning = false + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) - case - when new_record? + if new_record? "#{model_name.cache_key}/new" - when timestamp_names.any? - timestamp = max_updated_column_timestamp(timestamp_names) - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - when timestamp = max_updated_column_timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" else - "#{model_name.cache_key}/#{id}" + if cache_version && timestamp_names.none? + "#{model_name.cache_key}/#{id}" + else + timestamp = if timestamp_names.any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Specifying a timestamp name for #cache_key has been deprecated in favor of + the explicit #cache_version method that can be overwritten. + MSG + + max_updated_column_timestamp(timestamp_names) + else + max_updated_column_timestamp + end + + if timestamp + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{model_name.cache_key}/#{id}-#{timestamp}" + else + "#{model_name.cache_key}/#{id}" + end + end + end + end + + # Returns a cache version that can be used together with the cache key to form + # a recyclable caching scheme. By default, the #updated_at column is used for the + # cache_version, but this method can be overwritten to return something else. + # + # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to + # +false+ (which it is by default until Rails 6.0). + def cache_version + if cache_versioning && timestamp = try(:updated_at) + timestamp.utc.to_s(:usec) + end + end + + # Returns a cache key along with the version. + def cache_key_with_version + if version = cache_version + "#{cache_key}-#{version}" + else + cache_key end end diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 20d61dba67..25ee9d6bfe 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -23,7 +23,7 @@ module ActiveRecord end def table_exists? - ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } + connection.table_exists?(table_name) end # Creates an internal metadata table with columns +key+ and +value+ diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 8e8a97990a..3c7110369b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -51,8 +51,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :lock_optimistically, instance_writer: false - self.lock_optimistically = true + class_attribute :lock_optimistically, instance_writer: false, default: true end def locking_enabled? #:nodoc: @@ -60,13 +59,14 @@ module ActiveRecord end private + def increment_lock lock_col = self.class.locking_column previous_lock_value = send(lock_col).to_i send(lock_col + "=", previous_lock_value + 1) end - def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default @@ -75,23 +75,24 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) #:nodoc: + def _update_record(attribute_names = self.attribute_names) return super unless locking_enabled? return 0 if attribute_names.empty? - lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - increment_lock + begin + lock_col = self.class.locking_column - attribute_names += [lock_col] - attribute_names.uniq! + previous_lock_value = read_attribute_before_type_cast(lock_col) + + increment_lock + + attribute_names.push(lock_col) - begin relation = self.class.unscoped affected_rows = relation.where( self.class.primary_key => id, - lock_col => previous_lock_value, + lock_col => previous_lock_value ).update_all( attributes_for_update(attribute_names).map do |name| [name, _read_attribute(name)] @@ -104,9 +105,9 @@ module ActiveRecord affected_rows - # If something went wrong, revert the version. + # If something went wrong, revert the locking_column value. rescue Exception - send(lock_col + "=", previous_lock_value) + send(lock_col + "=", previous_lock_value.to_i) raise end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index e73cb4fc12..263e2a5f7f 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -59,7 +59,16 @@ module ActiveRecord # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns # the locked record. def lock!(lock = true) - reload(lock: lock) if persisted? + if persisted? + if changed? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Locking a record with unpersisted changes is deprecated and will raise an + exception in Rails 5.2. Use `save` to persist the changes, or `reload` to + discard them explicitly. + MSG + end + reload(lock: lock) + end self end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index f31931316c..2297c77835 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -15,31 +15,22 @@ module ActiveRecord rt end - def render_bind(attr, type_casted_value) - value = if attr.type.binary? && attr.value - "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" - else - type_casted_value - end - - [attr.name, value] - end - def sql(event) - return unless logger.debug? - self.class.runtime += event.duration + return unless logger.debug? payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) name = "#{payload[:name]} (#{event.duration.round(1)}ms)" + name = "CACHE #{name}" if payload[:cached] sql = payload[:sql] binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].zip(payload[:type_casted_binds]).map { |attr, value| + casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds]) + binds = " " + payload[:binds].zip(casted_params).map { |attr, value| render_bind(attr, value) }.inspect end @@ -52,6 +43,20 @@ module ActiveRecord private + def type_casted_binds(binds, casted_binds) + casted_binds || ActiveRecord::Base.connection.type_casted_binds(binds) + end + + def render_bind(attr, value) + if attr.is_a?(Array) + attr = attr.first + elsif attr.type.binary? && attr.value + value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + end + + [attr && attr.name, value] + end + def colorize_payload_name(name, payload_name) if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists color(name, MAGENTA, true) @@ -76,7 +81,7 @@ module ActiveRecord RED when /transaction\s*\Z/i CYAN - else + else MAGENTA end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 05568039d8..51c82f4ced 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,6 @@ require "set" +require "zlib" require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/regexp" module ActiveRecord class MigrationError < ActiveRecordError#:nodoc: @@ -277,8 +277,10 @@ module ActiveRecord # # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a - # default value for +column_name+ defined by +default+ on +table_name+. + # * <tt>change_column_default(table_name, column_name, default_or_changes)</tt>: + # Sets a default value for +column_name+ defined by +default_or_changes+ on + # +table_name+. Passing a hash containing <tt>:from</tt> and <tt>:to</tt> + # as +default_or_changes+ will make this change reversible in the migration. # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag # indicates whether the value can be +NULL+. See @@ -520,7 +522,10 @@ module ActiveRecord def self.inherited(subclass) # :nodoc: super if subclass.superclass == Migration - subclass.include Compatibility::Legacy + raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \ + "Please specify the Rails release the migration was written for:\n" \ + "\n" \ + " class #{subclass} < ActiveRecord::Migration[4.2]" end end @@ -543,12 +548,10 @@ module ActiveRecord end def call(env) - if connection.supports_migrations? - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending!(connection) - @last_check = mtime - end + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime end @app.call(env) end @@ -687,7 +690,7 @@ module ActiveRecord connection.respond_to?(:reverting) && connection.reverting end - class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: + ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc: def up yield unless reverting end @@ -767,7 +770,7 @@ module ActiveRecord when :down then announce "reverting" end - time = nil + time = nil ActiveRecord::Base.connection_pool.with_connection do |conn| time = Benchmark.measure do exec_migration(conn, direction) @@ -795,7 +798,7 @@ module ActiveRecord @connection = nil end - def write(text="") + def write(text = "") puts(text) if verbose end @@ -805,7 +808,7 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end - def say(message, subitem=false) + def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end @@ -933,7 +936,7 @@ module ActiveRecord # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy < Struct.new(:name, :version, :filename, :scope) + MigrationProxy = Struct.new(:name, :version, :filename, :scope) do def initialize(name, version, filename, scope) super @migration = nil @@ -989,11 +992,11 @@ module ActiveRecord end end - def rollback(migrations_paths, steps=1) + def rollback(migrations_paths, steps = 1) move(:down, migrations_paths, steps) end - def forward(migrations_paths, steps=1) + def forward(migrations_paths, steps = 1) move(:up, migrations_paths, steps) end @@ -1022,14 +1025,13 @@ module ActiveRecord def schema_migrations_table_name SchemaMigration.table_name end + deprecate :schema_migrations_table_name def get_all_versions(connection = Base.connection) - ActiveSupport::Deprecation.silence do - if connection.table_exists?(schema_migrations_table_name) - SchemaMigration.all.map { |x| x.version.to_i }.sort - else - [] - end + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end end @@ -1055,10 +1057,6 @@ module ActiveRecord Array(@migrations_paths) end - def match_to_migration_filename?(filename) # :nodoc: - Migration::MigrationFilenameRegexp.match?(File.basename(filename)) - end - def parse_migration_filename(filename) # :nodoc: File.basename(filename).scan(Migration::MigrationFilenameRegexp).first end @@ -1066,9 +1064,7 @@ module ActiveRecord def migrations(paths) paths = Array(paths) - files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] - - migrations = files.map do |file| + migrations = migration_files(paths).map do |file| version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version version = version.to_i @@ -1080,23 +1076,53 @@ module ActiveRecord migrations.sort_by(&:version) end + def migrations_status(paths) + paths = Array(paths) + + db_list = ActiveRecord::SchemaMigration.normalized_versions + + 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 + + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] + end + + (db_list + file_list).sort_by { |_, version, _| version } + end + + def migration_files(paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end + private def move(direction, migrations_paths, steps) migrator = new(direction, migrations(migrations_paths)) - start_index = migrator.migrations.index(migrator.current_migration) - if start_index - finish = migrator.migrations[start_index + steps] - version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + if current_version != 0 && !migrator.current_migration + raise UnknownMigrationVersionError.new(current_version) end + + start_index = + if current_version == 0 + 0 + else + migrator.migrations.index(migrator.current_migration) + end + + finish = migrator.migrations[start_index + steps] + version = finish ? finish.version : 0 + send(direction, migrations_paths, version) end end def initialize(direction, migrations, target_version = nil) - raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? - @direction = direction @target_version = target_version @migrated_versions = nil @@ -1104,8 +1130,8 @@ module ActiveRecord validate(@migrations) - Base.connection.initialize_schema_migrations_table - Base.connection.initialize_internal_metadata_table + ActiveRecord::SchemaMigration.create_table + ActiveRecord::InternalMetadata.create_table end def current_version @@ -1167,9 +1193,10 @@ module ActiveRecord def run_without_lock migration = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - execute_migration_in_transaction(migration, @direction) + result = execute_migration_in_transaction(migration, @direction) record_environment + result end # Used for running multiple migrations up to or down to a certain value. @@ -1178,11 +1205,12 @@ module ActiveRecord raise UnknownMigrationVersionError.new(@target_version) end - runnable.each do |migration| + result = runnable.each do |migration| execute_migration_in_transaction(migration, @direction) end record_environment + result end # Stores the current environment in the database. @@ -1230,10 +1258,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 diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 03103bba98..f9cf59b283 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -92,10 +92,6 @@ module ActiveRecord send(method, args, &block) end - def respond_to_missing?(*args) # :nodoc: - super || delegate.respond_to?(*args) - end - ReversibleAndIrreversibleMethods.each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -225,10 +221,14 @@ module ActiveRecord [:add_foreign_key, reversed_args] end + def respond_to_missing?(method, _) + super || delegate.respond_to?(method) + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) - if @delegate.respond_to?(method) - @delegate.send(method, *args, &block) + if delegate.respond_to?(method) + delegate.public_send(method, *args, &block) else super end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 04e538baa5..188dd0acef 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -11,9 +11,71 @@ module ActiveRecord const_get(name) end - V5_1 = Current + V5_2 = Current - module FourTwoShared + class V5_1 < V5_2 + end + + class V5_0 < V5_1 + module TableDefinition + def references(*args, **options) + super(*args, type: :integer, **options) + end + alias :belongs_to :references + end + + def create_table(table_name, options = {}) + if adapter_name == "PostgreSQL" + if options[:id] == :uuid && !options.key?(:default) + options[:default] = "uuid_generate_v4()" + end + end + + unless adapter_name == "Mysql2" && options[:id] == :bigint + if [:integer, :bigint].include?(options[:id]) && !options.key?(:default) + options[:default] = nil + end + end + + # Since 5.1 Postgres adapter uses bigserial type for primary + # keys by default and MySQL uses bigint. This compat layer makes old migrations utilize + # serial/int type instead -- the way it used to work before 5.1. + unless options.key?(:id) + options[:id] = :integer + end + + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def change_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def add_reference(table_name, ref_name, **options) + super(table_name, ref_name, type: :integer, **options) + end + alias :add_belongs_to :add_reference + end + + class V4_2 < V5_0 module TableDefinition def references(*, **options) options[:index] ||= false @@ -86,13 +148,13 @@ module ActiveRecord def index_name_for_remove(table_name, options = {}) index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) + unless index_name_exists?(table_name, index_name) if options.is_a?(Hash) && options.has_key?(:name) options_without_column = options.dup options_without_column.delete :column index_name_without_column = index_name(table_name, options_without_column) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + return index_name_without_column if index_name_exists?(table_name, index_name_without_column) end raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" @@ -101,29 +163,6 @@ module ActiveRecord index_name end end - - class V5_0 < V5_1 - end - - class V4_2 < V5_0 - # 4.2 is defined as a module because it needs to be shared with - # Legacy. When the time comes, V5_0 should be defined straight - # in its class. - include FourTwoShared - end - - module Legacy - include FourTwoShared - - def migrate(*) - ActiveSupport::Deprecation.warn \ - "Directly inheriting from ActiveRecord::Migration is deprecated. " \ - "Please specify the Rails release the migration was written for:\n" \ - "\n" \ - " class #{self.class.name} < ActiveRecord::Migration[4.2]" - super - end - end end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 76b3169411..1179a60e9b 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,78 +1,148 @@ +require "monitor" + module ActiveRecord module ModelSchema extend ActiveSupport::Concern + ## + # :singleton-method: primary_key_prefix_type + # :call-seq: primary_key_prefix_type + # + # The prefix type that will be prepended to every primary key column name. + # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + + ## + # :singleton-method: primary_key_prefix_type= + # :call-seq: primary_key_prefix_type=(prefix_type) + # + # Sets the prefix type that will be prepended to every primary key column name. + # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + + ## + # :singleton-method: table_name_prefix + # :call-seq: table_name_prefix + # + # The prefix string to prepend to every table name. + + ## + # :singleton-method: table_name_prefix= + # :call-seq: table_name_prefix=(prefix) + # + # Sets the prefix string to prepend to every table name. So if set to "basecamp_", all table + # names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient + # way of creating a namespace for tables in a shared database. By default, the prefix is the + # empty string. + # + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. + + ## + # :singleton-method: table_name_suffix + # :call-seq: table_name_suffix + # + # The suffix string to append to every table name. + + ## + # :singleton-method: table_name_suffix= + # :call-seq: table_name_suffix=(suffix) + # + # Works like +table_name_prefix=+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", + # "people_basecamp"). By default, the suffix is the empty string. + # + # If you are organising your models within modules, you can add a suffix to the models within + # a namespace by defining a singleton method in the parent module called table_name_suffix which + # returns your chosen suffix. + + ## + # :singleton-method: schema_migrations_table_name + # :call-seq: schema_migrations_table_name + # + # The name of the schema migrations table. By default, the value is <tt>"schema_migrations"</tt>. + + ## + # :singleton-method: schema_migrations_table_name= + # :call-seq: schema_migrations_table_name=(table_name) + # + # Sets the name of the schema migrations table. + + ## + # :singleton-method: internal_metadata_table_name + # :call-seq: internal_metadata_table_name + # + # The name of the internal metadata table. By default, the value is <tt>"ar_internal_metadata"</tt>. + + ## + # :singleton-method: internal_metadata_table_name= + # :call-seq: internal_metadata_table_name=(table_name) + # + # 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 + # + # Indicates whether table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a Product class will be "products". If false, it would just be "product". + # See table_name for the full rules on table/class naming. This is true, by default. + + ## + # :singleton-method: pluralize_table_names= + # :call-seq: pluralize_table_names=(value) + # + # Set whether table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a Product class will be "products". If false, it would just be "product". + # See table_name for the full rules on table/class naming. This is true, by default. + + ## + # :singleton-method: ignored_columns + # :call-seq: ignored_columns + # + # The list of columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + + ## + # :singleton-method: ignored_columns= + # :call-seq: ignored_columns=(columns) + # + # Sets the columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + included do - ## - # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the - # latter is specified, the Product class will look for "product_id" instead of "id". Remember - # that this is a global setting for all Active Records. mattr_accessor :primary_key_prefix_type, instance_writer: false - ## - # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set - # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", - # etc. This is a convenient way of creating a namespace for tables in a shared database. - # By default, the prefix is the empty string. - # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which - # returns your chosen prefix. - class_attribute :table_name_prefix, instance_writer: false - self.table_name_prefix = "" - - ## - # :singleton-method: - # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", - # "people_basecamp"). By default, the suffix is the empty string. - # - # If you are organising your models within modules, you can add a suffix to the models within - # a namespace by defining a singleton method in the parent module called table_name_suffix which - # returns your chosen suffix. - class_attribute :table_name_suffix, instance_writer: false - self.table_name_suffix = "" - - ## - # :singleton-method: - # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations" - class_attribute :schema_migrations_table_name, instance_accessor: false - self.schema_migrations_table_name = "schema_migrations" - - ## - # :singleton-method: - # Accessor for the name of the internal metadata table. By default, the value is "ar_internal_metadata" - class_attribute :internal_metadata_table_name, instance_accessor: false - self.internal_metadata_table_name = "ar_internal_metadata" - - ## - # :singleton-method: - # Accessor for an array of names of environments where destructive actions should be prohibited. By default, - # the value is ["production"] - class_attribute :protected_environments, instance_accessor: false - self.protected_environments = ["production"] - - ## - # :singleton-method: - # Indicates whether table names should be the pluralized versions of the corresponding class names. - # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. - # See table_name for the full rules on table/class naming. This is true, by default. - class_attribute :pluralize_table_names, instance_writer: false - self.pluralize_table_names = true - - ## - # :singleton-method: - # Accessor for the list of columns names the model should ignore. Ignored columns won't have attribute - # accessors defined, and won't be referenced in SQL queries. - class_attribute :ignored_columns, instance_accessor: false - self.ignored_columns = [].freeze + class_attribute :table_name_prefix, instance_writer: false, default: "" + 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 + class_attribute :ignored_columns, instance_accessor: false, default: [].freeze self.inheritance_column = "type" delegate :type_for_attribute, to: :class + + initialize_load_schema_monitor end # Derives the join table name for +first_table+ and +second_table+. The @@ -213,7 +283,7 @@ module ActiveRecord end # Sets the name of the sequence to use when generating ids to the given - # value, or (if the value is nil or false) to the value returned by the + # value, or (if the value is +nil+ or +false+) to the value returned by the # given block. This is required for Oracle and is useful for any # database which relies on sequences for primary key generation. # @@ -298,7 +368,7 @@ module ActiveRecord # default values when instantiating the Active Record object for this table. def column_defaults load_schema - _default_attributes.to_hash + @column_defaults ||= _default_attributes.to_hash end def _default_attributes # :nodoc: @@ -353,17 +423,30 @@ module ActiveRecord connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache + initialize_find_by_cache end + protected + + def initialize_load_schema_monitor + @load_schema_monitor = Monitor.new + end + private + def inherited(child_class) + super + child_class.initialize_load_schema_monitor + end + def schema_loaded? - defined?(@columns_hash) && @columns_hash + defined?(@schema_loaded) && @schema_loaded end def load_schema - unless schema_loaded? - load_schema! + return if schema_loaded? + @load_schema_monitor.synchronize do + load_schema! unless defined?(@columns_hash) && @columns_hash end end @@ -377,6 +460,8 @@ module ActiveRecord user_provided_default: false ) end + + @schema_loaded = true end def reload_schema_from_cache @@ -386,10 +471,12 @@ module ActiveRecord @attribute_types = nil @content_columns = nil @default_attributes = nil + @column_defaults = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil @columns = nil @columns_hash = nil + @schema_loaded = false @attribute_names = nil @yaml_encoder = nil direct_descendants.each do |descendant| diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e983026961..917bc76993 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -10,8 +10,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, instance_writer: false - self.nested_attributes_options = {} + class_attribute :nested_attributes_options, instance_writer: false, default: {} end # = Active Record Nested Attributes @@ -393,7 +392,7 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] if attributes.respond_to?(:permitted?) attributes = attributes.to_h end @@ -452,13 +451,13 @@ module ActiveRecord # { id: '2', _destroy: true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] if attributes_collection.respond_to?(:permitted?) attributes_collection = attributes_collection.to_h end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) - raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end check_record_limit!(options[:limit], attributes_collection) @@ -562,7 +561,7 @@ module ActiveRecord def call_reject_if(association_name, attributes) return false if will_be_destroyed?(association_name, attributes) - case callback = self.nested_attributes_options[association_name][:reject_if] + case callback = nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 254550c378..26966f9433 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -4,7 +4,7 @@ module ActiveRecord [] end - def delete_all(_conditions = nil) + def delete_all 0 end @@ -41,12 +41,11 @@ module ActiveRecord end def calculate(operation, _column_name) - if [:count, :sum].include? operation + case operation + when :count, :sum group_values.any? ? Hash.new : 0 - elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? - Hash.new - else - nil + when :average, :minimum, :maximum + group_values.any? ? Hash.new : nil end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 6933f3f9b8..f652c7c3a1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -100,6 +100,10 @@ module ActiveRecord !(@new_record || @destroyed) end + ## + # :call-seq: + # save(*args) + # # Saves the model. # # If the model is new, a record gets created in the database, otherwise @@ -121,12 +125,16 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*args) - create_or_update(*args) + def save(*args, &block) + create_or_update(*args, &block) rescue ActiveRecord::RecordInvalid false end + ## + # :call-seq: + # save!(*args) + # # Saves the model. # # If the model is new, a record gets created in the database, otherwise @@ -148,8 +156,10 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) + # + # Unless an error is raised, returns true. + def save!(*args, &block) + create_or_update(*args, &block) || raise(RecordNotSaved.new("Failed to save the record", self)) end # Deletes the record in the database and freezes this instance to @@ -181,7 +191,11 @@ module ActiveRecord _raise_readonly_record_error if readonly? destroy_associations self.class.connection.add_transaction_record(self) - destroy_row if persisted? + @_trigger_destroy_callback = if persisted? + destroy_row > 0 + else + true + end @destroyed = true freeze end @@ -253,7 +267,11 @@ module ActiveRecord verify_readonly_attribute(name) public_send("#{name}=", value) - changed? ? save(validate: false) : true + if has_changes_to_save? + save(validate: false) + else + true + end end # Updates the attributes of the model from the passed-in hash and saves the @@ -330,14 +348,16 @@ module ActiveRecord self end - # Wrapper around #increment that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def increment!(attribute, by = 1) + # Wrapper around #increment that writes the update to the database. + # Only +attribute+ is updated; the record itself is not saved. + # This means that any other modified attributes will still be dirty. + # Validations and callbacks are skipped. Supports the `touch` option from + # +update_counters+, see that for more. + # Returns +self+. + def increment!(attribute, by = 1, touch: nil) increment(attribute, by) - change = public_send(attribute) - (attribute_was(attribute.to_s) || 0) - self.class.update_counters(id, attribute => change) + change = public_send(attribute) - (attribute_in_database(attribute.to_s) || 0) + self.class.update_counters(id, attribute => change, touch: touch) clear_attribute_change(attribute) # eww self end @@ -349,12 +369,14 @@ module ActiveRecord increment(attribute, -by) end - # Wrapper around #decrement that saves the record. This method differs from - # its non-bang version in the sense that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def decrement!(attribute, by = 1) - increment!(attribute, -by) + # Wrapper around #decrement that writes the update to the database. + # Only +attribute+ is updated; the record itself is not saved. + # This means that any other modified attributes will still be dirty. + # Validations and callbacks are skipped. Supports the `touch` option from + # +update_counters+, see that for more. + # Returns +self+. + def decrement!(attribute, by = 1, touch: nil) + increment!(attribute, -by, touch: touch) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -384,8 +406,8 @@ module ActiveRecord # Reloads the record from the database. # - # This method finds record by its primary key (which could be assigned manually) and - # modifies the receiver in-place: + # This method finds the record by its primary key (which could be assigned + # manually) and modifies the receiver in-place: # # account = Account.new # # => #<Account id: nil, email: nil> @@ -515,6 +537,7 @@ module ActiveRecord raise ActiveRecord::StaleObjectError.new(self, "touch") end + @_trigger_update_callback = result result else true @@ -535,9 +558,9 @@ module ActiveRecord self.class.unscoped.where(self.class.primary_key => id) end - def create_or_update(*args) + def create_or_update(*args, &block) _raise_readonly_record_error if readonly? - result = new_record? ? _create_record : _update_record(*args) + result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end @@ -546,10 +569,16 @@ module ActiveRecord def _update_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? - 0 + rows_affected = 0 + @_trigger_update_callback = true else - self.class.unscoped._update_record attributes_values, id, id_was + rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database + @_trigger_update_callback = rows_affected > 0 end + + yield(self) if block_given? + + rows_affected end # Creates a record with values matching those of the instance attributes @@ -561,6 +590,9 @@ module ActiveRecord self.id ||= new_id if self.class.primary_key @new_record = false + + yield(self) if block_given? + id end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 387dd8e9bd..ec246e97bc 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -24,26 +24,24 @@ module ActiveRecord end def self.run - connection = ActiveRecord::Base.connection - enabled = connection.query_cache_enabled - connection.enable_query_cache! + caching_pool = ActiveRecord::Base.connection_pool + caching_was_enabled = caching_pool.query_cache_enabled - enabled + caching_pool.enable_query_cache! + + [caching_pool, caching_was_enabled] end - def self.complete(enabled) - ActiveRecord::Base.connection.clear_query_cache - ActiveRecord::Base.connection.disable_query_cache! unless enabled + def self.complete((caching_pool, caching_was_enabled)) + caching_pool.disable_query_cache! unless caching_was_enabled + + ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| + pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? + end end def self.install_executor_hooks(executor = ActiveSupport::Executor) executor.register_hook(self) - - executor.to_complete do - unless ActiveRecord::Base.connected? && ActiveRecord::Base.connection.transaction_open? - ActiveRecord::Base.clear_active_connections! - end - end end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 36689f6559..b16e178358 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,8 +8,8 @@ module ActiveRecord delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, - :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :merge, to: :all + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, + :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 989d23bc37..73518ca144 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -82,15 +82,15 @@ module ActiveRecord if config.active_record.delete(:use_schema_cache_dump) config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) - cache = Marshal.load File.binread filename + cache = YAML.load(File.read(filename)) if cache.version == ActiveRecord::Migrator.current_version - self.connection.schema_cache = cache - self.connection_pool.schema_cache = cache.dup + connection.schema_cache = cache + connection_pool.schema_cache = cache.dup else - warn "Ignoring db/schema_cache.dump 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 #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end end end @@ -108,7 +108,7 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - app.config.active_record.each do |k,v| + app.config.active_record.each do |k, v| send "#{k}=", v end end @@ -166,5 +166,13 @@ end_warning path = app.paths["db"].first config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"] end + + initializer "active_record.clear_active_connections" do + config.after_initialize do + ActiveSupport.on_load(:active_record) do + clear_active_connections! + end + end + end end end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index adb3c6c4e6..8658188623 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -6,10 +6,14 @@ module ActiveRecord module ControllerRuntime #:nodoc: extend ActiveSupport::Concern + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_internal :db_runtime + private + def process_action(action, *args) # We also need to reset the runtime before each action # because of queries in middleware or in cases we are streaming diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 46235ab922..711099e9e1 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -77,6 +77,8 @@ 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 + raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? + if ENV["VERSION"] db_namespace["migrate:down"].invoke db_namespace["migrate:up"].invoke @@ -91,16 +93,17 @@ db_namespace = namespace :db do # desc 'Runs the "up" for a given migration VERSION.' task up: [:environment, :load_config] do + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - raise "VERSION is required" unless version ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace["_dump"].invoke end # desc 'Runs the "down" for a given migration VERSION.' task down: [:environment, :load_config] do + raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - raise "VERSION is required - To go down one migration, run db:rollback" unless version ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace["_dump"].invoke end @@ -110,28 +113,13 @@ db_namespace = namespace :db do unless ActiveRecord::SchemaMigration.table_exists? abort "Schema migrations table does not exist yet." end - db_list = ActiveRecord::SchemaMigration.normalized_versions - - file_list = - ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| - Dir.foreach(path).map do |file| - next unless ActiveRecord::Migrator.match_to_migration_filename?(file) - - version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file) - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact - end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end # output puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths + ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -265,19 +253,16 @@ db_namespace = namespace :db do end namespace :cache do - desc "Creates a db/schema_cache.dump file." + desc "Creates a db/schema_cache.yml file." task dump: [:environment, :load_config] do - con = ActiveRecord::Base.connection - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - - con.schema_cache.clear! - con.data_sources.each { |table| con.schema_cache.add(table) } - open(filename, "wb") { |f| f.write(Marshal.dump(con.schema_cache)) } + 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.dump file." + desc "Clears a db/schema_cache.yml file." task clear: [:environment, :load_config] do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") rm_f filename, verbose: false end end @@ -291,8 +276,7 @@ db_namespace = namespace :db do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - if ActiveRecord::Base.connection.supports_migrations? && - ActiveRecord::SchemaMigration.table_exists? + if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information f.print "\n" @@ -312,14 +296,6 @@ db_namespace = namespace :db do end namespace :test do - - task :deprecated do - Rake.application.top_level_tasks.grep(/^db:test:/).each do |task| - $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \ - "your test schema automatically, see the release notes for details." - end - end - # desc "Recreate the test database from the current schema" task load: %w(db:test:purge) do case ActiveRecord::Base.schema_format @@ -348,22 +324,6 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"] end - # desc "Recreate the test database from a fresh schema" - task clone: %w(db:test:deprecated environment) do - case ActiveRecord::Base.schema_format - when :ruby - db_namespace["test:clone_schema"].invoke - when :sql - db_namespace["test:clone_structure"].invoke - end - end - - # desc "Recreate the test database from a fresh schema.rb file" - task clone_schema: %w(db:test:deprecated db:schema:dump db:test:load_schema) - - # desc "Recreate the test database from a fresh structure.sql file" - task clone_structure: %w(db:test:deprecated db:structure:dump db:test:load_structure) - # desc "Empty the test database" task purge: %w(environment load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 8ff265bdfa..af6473d250 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -3,15 +3,14 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_accessor: false - self._attr_readonly = [] + class_attribute :_attr_readonly, instance_accessor: false, default: [] end module ClassMethods # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || []) + self._attr_readonly = Set.new(attributes.map(&:to_s)) + (_attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 9b692f55d2..e8ee8279fd 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,6 @@ require "thread" require "active_support/core_ext/string/filters" +require "active_support/deprecation" module ActiveRecord # = Active Record Reflection @@ -7,10 +8,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections, instance_writer: false - class_attribute :aggregate_reflections, instance_writer: false - self._reflections = {} - self.aggregate_reflections = {} + class_attribute :_reflections, instance_writer: false, default: {} + class_attribute :aggregate_reflections, instance_writer: false, default: {} end def self.create(macro, name, scope, options, ar) @@ -136,8 +135,8 @@ module ActiveRecord # BelongsToReflection # HasAndBelongsToManyReflection # ThroughReflection - # PolymorphicReflection - # RuntimeReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: def through_reflection? false @@ -171,12 +170,47 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - def join_keys(association_klass) - JoinKeys.new(foreign_key, active_record_primary_key) + def join_keys + get_join_keys klass + end + + # Returns a list of scopes that should be applied for this Reflection + # object when querying the database. + def scopes + scope ? [scope] : [] + end + + def scope_chain + chain.map(&:scopes) + end + deprecate :scope_chain + + def join_scopes(table, predicate_builder) # :nodoc: + if scope + [ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(&scope)] + else + [] + end + end + + def klass_join_scope(table, predicate_builder) # :nodoc: + if klass.current_scope + klass.current_scope.clone.tap { |scope| + scope.joins_values = scope.left_outer_joins_values = [].freeze + } + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end end def constraints - scope_chain.flatten + chain.flat_map(&:scopes) end def counter_cache_column @@ -248,6 +282,25 @@ module ActiveRecord def chain collect_join_chain end + + def get_join_keys(association_klass) + JoinKeys.new(join_pk(association_klass), join_fk) + end + + protected + def actual_source_reflection # FIXME: this is a horrible name + self + end + + private + + def join_pk(_) + foreign_key + end + + def join_fk + active_record_primary_key + end end # Base class for AggregateReflection and AssociationReflection. Objects of @@ -282,7 +335,6 @@ module ActiveRecord end def autosave=(autosave) - @automatic_inverse_of = false @options[:autosave] = autosave parent_reflection = self.parent_reflection if parent_reflection @@ -322,7 +374,7 @@ module ActiveRecord end end - # Holds all the meta-data about an aggregation as it was specified in the + # Holds all the metadata about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: def mapping @@ -331,7 +383,7 @@ module ActiveRecord end end - # Holds all the meta-data about an association as it was specified in the + # Holds all the metadata about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: # Returns the target association's class. @@ -365,6 +417,17 @@ module ActiveRecord @constructable = calculate_constructable(macro, options) @association_scope_cache = {} @scope_lock = Mutex.new + + if options[:class_name] && options[:class_name].class == Class + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class to the `class_name` is deprecated and will raise + an ArgumentError in Rails 5.2. It eagerloads more classes than + necessary and potentially creates circular dependencies. + + Please pass the class name as a string: + `#{macro} :#{name}, class_name: '#{options[:class_name]}'` + MSG + end end def association_scope_cache(conn, owner) @@ -398,6 +461,10 @@ 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 @@ -447,12 +514,6 @@ module ActiveRecord false end - # An array of arrays of scopes. Each item in the outside array corresponds to a reflection - # in the #chain. - def scope_chain - scope ? [[scope]] : [[]] - end - def has_scope? scope end @@ -523,11 +584,9 @@ module ActiveRecord seed + [self] end - protected - - def actual_source_reflection # FIXME: this is a horrible name - self - end + def extensions + Array(options[:extend]) + end private @@ -537,14 +596,10 @@ module ActiveRecord # Attempts to find the inverse association name automatically. # If it cannot find a suitable inverse association name, it returns - # nil. + # +nil+. def inverse_name options.fetch(:inverse_of) do - if @automatic_inverse_of == false - nil - else - @automatic_inverse_of ||= automatic_inverse_of - end + @automatic_inverse_of ||= automatic_inverse_of end end @@ -671,11 +726,6 @@ module ActiveRecord end end - def join_keys(association_klass) - key = polymorphic? ? association_primary_key(association_klass) : association_primary_key - JoinKeys.new(key, foreign_key) - end - def join_id_for(owner) # :nodoc: owner[foreign_key] end @@ -685,6 +735,14 @@ module ActiveRecord def calculate_constructable(macro, options) !polymorphic? end + + def join_fk + foreign_key + end + + def join_pk(klass) + polymorphic? ? association_primary_key(klass) : association_primary_key + end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: @@ -699,16 +757,15 @@ module ActiveRecord end end - # Holds all the meta-data about a :through association as it was specified + # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: - attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, - :active_record_primary_key, :type, to: :source_reflection + :active_record_primary_key, :type, :get_join_keys, to: :source_reflection def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:anonymous_class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end @@ -786,45 +843,16 @@ module ActiveRecord through_reflection.clear_association_scope_cache end - # Consider the following example: - # - # class Person - # has_many :articles - # has_many :comment_tags, through: :articles - # end - # - # class Article - # has_many :comments - # has_many :comment_tags, through: :comments, source: :tags - # end - # - # class Comment - # has_many :tags - # end - # - # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags, - # but only Comment.tags will be represented in the #chain. So this method creates an array - # of scopes corresponding to the chain. - def scope_chain - @scope_chain ||= begin - scope_chain = source_reflection.scope_chain.map(&:dup) - - # Add to it the scope from this reflection (if any) - scope_chain.first << scope if scope - - through_scope_chain = through_reflection.scope_chain.map(&:dup) + def scopes + source_reflection.scopes + super + end - if options[:source_type] - type = foreign_type - source_type = options[:source_type] - through_scope_chain.first << lambda { |object| - where(type => source_type) - } - end + def join_scopes(table, predicate_builder) # :nodoc: + source_reflection.join_scopes(table, predicate_builder) + super + end - # Recursively fill out the rest of the array from the through reflection - scope_chain + through_scope_chain - end + def source_type_scope + through_reflection.klass.where(foreign_type => options[:source_type]) end def has_scope? @@ -833,10 +861,6 @@ module ActiveRecord through_reflection.has_scope? end - def join_keys(association_klass) - source_reflection.join_keys(association_klass) - end - # A through association is nested if there would be more than one join table def nested? source_reflection.through_reflection? || through_reflection.through_reflection? @@ -851,6 +875,10 @@ 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 @@ -875,15 +903,13 @@ module ActiveRecord } if names.length > 1 - example_options = options.dup - example_options[:source] = source_reflection_names.first - ActiveSupport::Deprecation.warn \ - "Ambiguous source reflection for through association. Please " \ - "specify a :source directive on your declaration like:\n" \ - "\n" \ - " class #{active_record.name} < ActiveRecord::Base\n" \ - " #{macro} :#{name}, #{example_options}\n" \ - " end" + raise AmbiguousSourceReflectionForThroughAssociation.new( + active_record.name, + macro, + name, + options, + source_reflection_names + ) end @source_reflection_name = names.first @@ -930,6 +956,14 @@ module ActiveRecord raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end + if parent_reflection.nil? + reflections = active_record.reflections.keys.map(&:to_sym) + + if reflections.index(through_reflection.name) > reflections.index(name) + raise HasManyThroughOrderError.new(active_record.name, self, through_reflection) + end + end + check_validity_of_inverse! end @@ -951,19 +985,23 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - def collect_join_reflections(seed) - a = source_reflection.add_as_source seed - if options[:source_type] - through_reflection.add_as_polymorphic_through self, a - else - through_reflection.add_as_through a - end - end - + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected + attr_reader :delegate_reflection def actual_source_reflection # FIXME: this is a horrible name - source_reflection.send(:actual_source_reflection) + source_reflection.actual_source_reflection + end + + private + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end end def primary_key(klass) @@ -972,7 +1010,6 @@ module ActiveRecord def inverse_name; delegate_reflection.send(:inverse_name); end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name @@ -984,12 +1021,30 @@ module ActiveRecord delegate(*delegate_methods, to: :delegate_reflection) end - class PolymorphicReflection < ThroughReflection # :nodoc: + class PolymorphicReflection < AbstractReflection # :nodoc: def initialize(reflection, previous_reflection) @reflection = reflection @previous_reflection = previous_reflection end + def scopes + scopes = @previous_reflection.scopes + if @previous_reflection.options[:source_type] + scopes + [@previous_reflection.source_type_scope] + else + scopes + end + end + + def join_scopes(table, predicate_builder) # :nodoc: + scopes = @previous_reflection.join_scopes(table, predicate_builder) + super + if @previous_reflection.options[:source_type] + scopes + [@previous_reflection.source_type_scope] + else + scopes + end + end + def klass @reflection.klass end @@ -1006,10 +1061,6 @@ module ActiveRecord @reflection.plural_name end - def join_keys(association_klass) - @reflection.join_keys(association_klass) - end - def type @reflection.type end @@ -1023,6 +1074,10 @@ module ActiveRecord source_type = @previous_reflection.options[:source_type] lambda { |object| where(type => source_type) } end + + def get_join_keys(association_klass) + @reflection.get_join_keys(association_klass) + end end class RuntimeReflection < PolymorphicReflection # :nodoc: @@ -1054,7 +1109,7 @@ module ActiveRecord end def alias_name - Arel::Table.new(table_name) + Arel::Table.new(table_name, type_caster: klass.type_caster) end def all_includes; yield; end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ef629dcb3b..7a8f9abb36 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -2,7 +2,7 @@ module ActiveRecord # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :left_joins, :left_outer_joins, :references, + :order, :joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded + alias :locked? :locked def initialize(klass, table, predicate_builder, values = {}) @klass = klass @@ -261,10 +262,6 @@ module ActiveRecord coder.represent_seq(nil, records) end - def as_json(options = nil) #:nodoc: - records.as_json(options) - end - # Returns size of the records. def size loaded? ? @records.length : count(:all) @@ -273,8 +270,7 @@ module ActiveRecord # Returns true if there are no records. def empty? return @records.empty? if loaded? - - limit_value == 0 || !exists? + !exists? end # Returns true if there are no records. @@ -362,6 +358,9 @@ module ActiveRecord # # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David') + # + # # Update all invoices and set the number column to its id value. + # Invoice.update_all('number = id') def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? @@ -370,7 +369,7 @@ module ActiveRecord stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) - if joins_values.any? + if has_join_values? @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else stmt.key = arel_attribute(primary_key) @@ -405,9 +404,9 @@ module ActiveRecord # # Note: Updating a large number of records will run an # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use #update_all for updating all records using - # a single query. + # issue. When running callbacks is not needed for each record update, + # it is preferred to use #update_all 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]) } @@ -415,8 +414,7 @@ module ActiveRecord records.each { |record| record.update(attributes) } else if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) + raise ArgumentError, <<-MSG.squish You are passing an instance of ActiveRecord::Base to `update`. Please pass the id of the object by calling `.id`. MSG @@ -443,16 +441,8 @@ module ActiveRecord # ==== Examples # # Person.where(age: 0..18).destroy_all - def destroy_all(conditions = nil) - if conditions - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1. - To achieve the same use where(conditions).destroy_all. - MESSAGE - where(conditions).destroy_all - else - records.each(&:destroy).tap { reset } - end + def destroy_all + records.each(&:destroy).tap { reset } end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, @@ -500,7 +490,7 @@ module ActiveRecord # # Post.limit(100).delete_all # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit - def delete_all(conditions = nil) + def delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| value = get_value(method) SINGLE_VALUE_METHODS.include?(method) ? value : value.any? @@ -509,27 +499,19 @@ module ActiveRecord raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") end - if conditions - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Passing conditions to delete_all is deprecated and will be removed in Rails 5.1. - To achieve the same use where(conditions).delete_all. - MESSAGE - where(conditions).delete_all - else - stmt = Arel::DeleteManager.new - stmt.from(table) + stmt = Arel::DeleteManager.new + stmt.from(table) - if joins_values.any? - @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) - else - stmt.wheres = arel.constraints - end + if has_join_values? + @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) + else + stmt.wheres = arel.constraints + end - affected = @klass.connection.delete(stmt, "SQL", bound_attributes) + affected = @klass.connection.delete(stmt, "SQL", bound_attributes) - reset - affected - end + reset + affected end # Deletes the row with a primary key matching the +id+ argument, using a @@ -627,15 +609,6 @@ module ActiveRecord includes_values & joins_values end - # {#uniq}[rdoc-ref:QueryMethods#uniq] and - # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated. - # #uniq_value delegates to #distinct_value to maintain backwards compatibility. - # Use #distinct_value instead. - def uniq_value - distinct_value - end - deprecate uniq_value: :distinct_value - # Compares two relations for equality. def ==(other) case other @@ -662,7 +635,9 @@ module ActiveRecord end def inspect - entries = records.take([limit_value, 11].compact.min).map!(&:inspect) + subject = loaded? ? records : self + entries = subject.take([limit_value, 11].compact.min).map!(&:inspect) + entries[10] = "..." if entries.size == 11 "#<#{self.class.name} [#{entries.join(', ')}]>" @@ -677,13 +652,18 @@ module ActiveRecord private + def has_join_values? + joins_values.any? || left_outer_joins_values.any? + end + def exec_queries(&block) @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze preload = preload_values - preload += includes_values unless eager_loading? - preloader = build_preloader + preload += includes_values unless eager_loading? + preloader = nil preload.each do |associations| + preloader ||= build_preloader preloader.preload @records, associations end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4b2987ac6d..13a2c3f511 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -30,14 +30,14 @@ module ActiveRecord # end # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -89,14 +89,14 @@ module ActiveRecord # To be yielded each record one by one, use #find_each instead. # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -140,9 +140,9 @@ module ActiveRecord # If you do not provide a block to #in_batches, it will return a # BatchEnumerator which is enumerable. # - # Person.in_batches.with_index do |relation, batch_index| + # Person.in_batches.each_with_index do |relation, batch_index| # puts "Processing relation ##{batch_index}" - # relation.each { |relation| relation.delete_all } + # relation.delete_all # end # # Examples of calling methods on the returned BatchEnumerator object: @@ -152,12 +152,12 @@ module ActiveRecord # Person.in_batches.each_record(&:party_all_night!) # # ==== Options - # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch # size, it can be less than, equal, or greater than the limit. @@ -186,7 +186,7 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # consistent. Therefore the primary key must be orderable, e.g an integer + # consistent. Therefore the primary key must be orderable, e.g. an integer # or a string. # # NOTE: By its nature, batch processing is subject to race conditions if @@ -260,7 +260,7 @@ module ActiveRecord end def act_on_ignored_order(error_on_ignore) - raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order : error_on_ignore) + raise_error = (error_on_ignore.nil? ? klass.error_on_ignored_order : error_on_ignore) if raise_error raise ArgumentError.new(ORDER_IGNORE_MESSAGE) diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 333b3a63cf..3555779ec2 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -7,7 +7,7 @@ module ActiveRecord @of = of @relation = relation @start = start - @finish = finish + @finish = finish end # Looping through a collection of records from the database (using the diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index a887be8a20..c562f214c9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -38,10 +38,16 @@ module ActiveRecord # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) if block_given? - to_a.count { |*block_args| yield(*block_args) } - else - calculate(:count, column_name) + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `count' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() end + + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's @@ -75,8 +81,17 @@ module ActiveRecord # #calculate for examples with options. # # Person.sum(:age) # => 4562 - def sum(column_name = nil, &block) - return super(&block) if block_given? + def sum(column_name = nil) + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `sum' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:sum, column_name) end @@ -112,10 +127,6 @@ module ActiveRecord # ... # end def calculate(operation, column_name) - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - column_name = attribute_alias(column_name) - end - if has_include?(column_name) relation = construct_relation_for_association_calculations relation = relation.distinct if operation.to_s.downcase == "count" @@ -197,7 +208,7 @@ module ActiveRecord # If #count is used with #distinct (i.e. `relation.distinct.count`) it is # considered distinct. - distinct = self.distinct_value + distinct = distinct_value if operation == "count" column_name ||= select_for_count @@ -215,8 +226,8 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped.table, column_name) + if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) end @@ -227,17 +238,17 @@ module ActiveRecord end def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - # PostgreSQL doesn't like ORDER BY when there are no GROUP BY - relation = unscope(:order) - column_alias = column_name - if operation == "count" && (relation.limit_value || relation.offset_value) + if operation == "count" && (limit_value || offset_value) # Shortcut when limit is zero. - return 0 if relation.limit_value == 0 + return 0 if limit_value == 0 - query_builder = build_count_subquery(relation, column_name, distinct) + query_builder = build_count_subquery(spawn, column_name, distinct) else + # PostgreSQL doesn't like ORDER BY when there are no GROUP BY + relation = unscope(:order).distinct!(false) + column = aggregate_column(column_name) select_value = operation_over_aggregate_column(column, operation, distinct) @@ -286,7 +297,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += select_values unless having_clause.empty? + select_values += self.select_values unless having_clause.empty? select_values.concat group_columns.map { |aliaz, field| if field.respond_to?(:as) @@ -296,7 +307,7 @@ module ActiveRecord end } - relation = except(:group) + relation = except(:group).distinct!(false) relation.group_values = group_fields relation.select_values = select_values @@ -375,9 +386,8 @@ module ActiveRecord relation.select_values = [aliased_column] subquery = relation.arel.as(subquery_alias) - sm = Arel::SelectManager.new relation.engine select_value = operation_over_aggregate_column(column_alias, "count", distinct) - sm.project(select_value).from(subquery) + Arel::SelectManager.new(subquery).project(select_value) end end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d16de4b06c..dada0fddf8 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,6 +1,3 @@ -require "active_support/concern" -require "active_support/core_ext/regexp" - module ActiveRecord module Delegation # :nodoc: module DelegateCache # :nodoc: @@ -18,7 +15,10 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub("::".freeze, "_".freeze), delegate + mangled_name = klass.name.gsub("::".freeze, "_".freeze) + const_set mangled_name, delegate + private_constant mangled_name + cache[klass] = delegate end end @@ -36,13 +36,16 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join, :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :to_sentence, :to_formatted_s, :as_json, :shuffle, :split, :index, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, to: :klass + delegate :ast, :locked, to: :arel + module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern @@ -81,7 +84,7 @@ module ActiveRecord end end - protected + private def method_missing(method, *args, &block) if @klass.respond_to?(method) @@ -108,12 +111,10 @@ module ActiveRecord end end - def respond_to_missing?(method, include_private = false) - super || @klass.respond_to?(method, include_private) || - arel.respond_to?(method, include_private) - end - - protected + private + def respond_to_missing?(method, _) + super || @klass.respond_to?(method) || arel.respond_to?(method) + end def method_missing(method, *args, &block) if @klass.respond_to?(method) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 5e580ac865..1d661fa8ed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -17,8 +17,8 @@ module ActiveRecord # 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'd need to provide an explicit QueryMethods#order - # option if you want the results are sorted. + # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order + # option if you want the results to be sorted. # # ==== Find with lock # @@ -76,7 +76,7 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(arg, *args) where(arg, *args).take - rescue RangeError + rescue ::RangeError nil end @@ -84,7 +84,7 @@ module ActiveRecord # an ActiveRecord::RecordNotFound error. def find_by!(arg, *args) where(arg, *args).take! - rescue RangeError + rescue ::RangeError raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", @klass.name) end @@ -147,19 +147,11 @@ module ActiveRecord def last(limit = nil) return find_last(limit) if loaded? || limit_value - result = limit(limit || 1) + result = limit(limit) result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key result = result.reverse_order! limit ? result.reverse : result.first - rescue ActiveRecord::IrreversibleOrderError - ActiveSupport::Deprecation.warn(<<-WARNING.squish) - Finding a last element by loading the relation when SQL ORDER - can not be reversed is deprecated. - Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. - Please call `to_a.last` if you still want to load the relation. - WARNING - find_last(limit) end # Same as #last but raises ActiveRecord::RecordNotFound if no record @@ -309,31 +301,23 @@ module ActiveRecord # Person.exists? def exists?(conditions = :none) if Base === conditions - conditions = conditions.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) + raise ArgumentError, <<-MSG.squish You are passing an instance of ActiveRecord::Base to `exists?`. Please pass the id of the object by calling `.id`. MSG end - return false if !conditions + return false if !conditions || limit_value == 0 - relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - return false if ActiveRecord::NullRelation === relation + relation = self unless eager_loading? + relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) + return false if ActiveRecord::NullRelation === relation - case conditions - when Array, Hash - relation = relation.where(conditions) - else - unless conditions == :none - relation = relation.where(primary_key => conditions) - end - end + relation = construct_relation_for_exists(relation, conditions) connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false - rescue RangeError + rescue ::RangeError false end @@ -345,7 +329,7 @@ module ActiveRecord # of results obtained should be provided in the +result_size+ argument and # the expected number of results should be provided in the +expected_size+ # argument. - def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil) # :nodoc: + def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key) # :nodoc: conditions = arel.where_sql(@klass.arel_engine) conditions = " [#{conditions}]" if conditions name = @klass.name @@ -353,15 +337,15 @@ module ActiveRecord if ids.nil? error = "Couldn't find #{name}" error << " with#{conditions}" if conditions - raise RecordNotFound, error + raise RecordNotFound.new(error, name) elsif Array(ids).size == 1 - error = "Couldn't find #{name} with '#{primary_key}'=#{ids}#{conditions}" - raise RecordNotFound.new(error, name, primary_key, ids) + error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" + raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{name.pluralize} with '#{primary_key}': " + error = "Couldn't find all #{name.pluralize} with '#{key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" - raise RecordNotFound, error + raise RecordNotFound.new(error, name, primary_key, ids) end end @@ -400,6 +384,19 @@ module ActiveRecord end end + def construct_relation_for_exists(relation, conditions) + relation = relation.except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + + case conditions + when Array, Hash + relation.where!(conditions) + else + relation.where!(primary_key => conditions) unless conditions == :none + end + + relation + end + def construct_join_dependency(joins = [], eager_loading: true) including = eager_load_values + includes_values ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading) @@ -410,8 +407,7 @@ module ActiveRecord end def apply_join_dependency(relation, join_dependency) - relation = relation.except(:includes, :eager_load, :preload) - relation = relation.joins join_dependency + relation = relation.except(:includes, :eager_load, :preload).joins!(join_dependency) if using_limitable_reflections?(join_dependency.reflections) relation @@ -439,8 +435,6 @@ module ActiveRecord reflections.none?(&:collection?) end - protected - def find_with_ids(*ids) raise UnknownPrimaryKey.new(@klass) if primary_key.nil? @@ -458,14 +452,13 @@ module ActiveRecord else find_some(ids) end - rescue RangeError + rescue ::RangeError raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" end def find_one(id) if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) + raise ArgumentError, <<-MSG.squish You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`. MSG @@ -548,8 +541,12 @@ module ActiveRecord self end - relation = relation.offset(offset_index + index) unless index.zero? - relation.limit(limit).to_a + if limit_value.nil? || index < limit_value + relation = relation.offset(offset_index + index) unless index.zero? + relation.limit(limit).to_a + else + [] + end end end @@ -572,8 +569,6 @@ module ActiveRecord end end - private - def find_last(limit) limit ? records.last(limit) : records.last end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 780a1ee422..a6309e0b5c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,14 +1,5 @@ module ActiveRecord class PredicateBuilder # :nodoc: - require "active_record/relation/predicate_builder/array_handler" - require "active_record/relation/predicate_builder/association_query_handler" - require "active_record/relation/predicate_builder/base_handler" - require "active_record/relation/predicate_builder/basic_object_handler" - require "active_record/relation/predicate_builder/class_handler" - require "active_record/relation/predicate_builder/polymorphic_array_handler" - require "active_record/relation/predicate_builder/range_handler" - require "active_record/relation/predicate_builder/relation_handler" - delegate :resolve_column_aliases, to: :table def initialize(table) @@ -16,14 +7,11 @@ module ActiveRecord @handlers = [] register_handler(BasicObject, BasicObjectHandler.new) - register_handler(Class, ClassHandler.new(self)) register_handler(Base, BaseHandler.new(self)) register_handler(Range, RangeHandler.new) register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) - register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) - register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) end def build_from_hash(attributes) @@ -66,6 +54,8 @@ module ActiveRecord handler_for(value).call(attribute, value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :table @@ -92,9 +82,27 @@ module ActiveRecord attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) result[column_name] = attrs binds += bvs - next - when value.is_a?(Relation) - binds += value.bound_attributes + when table.associated_with?(column_name) + # Find the foreign key when using queries such as: + # Post.where(author: author) + # + # For polymorphic relationships, find the foreign key and type: + # PriceEstimate.where(estimate_of: treasure) + associated_table = table.associated_table(column_name) + if associated_table.polymorphic_association? + case value.is_a?(Array) ? value.first : value + when Base, Relation + value = [value] unless value.is_a?(Array) + klass = PolymorphicArrayValue + end + end + + klass ||= AssociationQueryValue + result[column_name] = klass.new(associated_table, value).queries.map do |query| + attrs, bvs = create_binds_for_hash(query) + binds.concat(bvs) + attrs + end when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype) first = value.begin last = value.end @@ -112,17 +120,10 @@ module ActiveRecord if can_be_bound?(column_name, value) result[column_name] = Arel::Nodes::BindParam.new binds << build_bind_param(column_name, value) + elsif value.is_a?(Relation) + binds.concat(value.bound_attributes) end end - - # Find the foreign key when using queries such as: - # Post.where(author: author) - # - # For polymorphic relationships, find the foreign key and type: - # PriceEstimate.where(estimate_of: treasure) - if table.associated_with?(column_name) - result[column_name] = AssociationQueryHandler.value_for(table, column_name, value) - end end [result, binds] @@ -155,7 +156,6 @@ module ActiveRecord end def can_be_bound?(column_name, value) - return if table.associated_with?(column_name) case value when Array, Range table.type(column_name).respond_to?(:subtype) @@ -169,3 +169,12 @@ module ActiveRecord end end end + +require "active_record/relation/predicate_builder/array_handler" +require "active_record/relation/predicate_builder/base_handler" +require "active_record/relation/predicate_builder/basic_object_handler" +require "active_record/relation/predicate_builder/range_handler" +require "active_record/relation/predicate_builder/relation_handler" + +require "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 6400caba06..1068e700e2 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -6,11 +6,11 @@ module ActiveRecord end def call(attribute, value) + return attribute.in([]) if value.empty? + return queries_predicates(value) if value.all? { |v| v.is_a?(Hash) } + values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) - - return attribute.in([]) if values.empty? && nils.empty? - ranges, values = values.partition { |v| v.is_a?(Range) } values_predicate = @@ -26,9 +26,11 @@ module ActiveRecord array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) - array_predicates.inject { |composite, predicate| composite.or(predicate) } + array_predicates.inject(&:or) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :predicate_builder @@ -38,6 +40,17 @@ module ActiveRecord other end end + + private + def queries_predicates(queries) + if queries.size > 1 + queries.map do |query| + Arel::Nodes::And.new(predicate_builder.build_from_hash(query)) + end.inject(&:or) + else + predicate_builder.build_from_hash(queries.first) + end + end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb deleted file mode 100644 index 7e20cb2c63..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ /dev/null @@ -1,89 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class AssociationQueryHandler # :nodoc: - def self.value_for(table, column, value) - associated_table = table.associated_table(column) - klass = if associated_table.polymorphic_association? && ::Array === value && value.first.is_a?(Base) - PolymorphicArrayValue - else - AssociationQueryValue - end - - klass.new(associated_table, value) - end - - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - queries = {} - - table = value.associated_table - if value.base_class - queries[table.association_foreign_type.to_s] = value.base_class.name - end - - queries[table.association_foreign_key.to_s] = value.ids - predicate_builder.build_from_hash(queries) - end - - protected - - attr_reader :predicate_builder - end - - class AssociationQueryValue # :nodoc: - attr_reader :associated_table, :value - - def initialize(associated_table, value) - @associated_table = associated_table - @value = value - end - - def ids - case value - when Relation - value.select(primary_key) - when Array - value.map { |v| convert_to_id(v) } - else - convert_to_id(value) - end - end - - def base_class - if associated_table.polymorphic_association? - @base_class ||= polymorphic_base_class_from_value - end - end - - private - - def primary_key - associated_table.association_primary_key(base_class) - end - - def polymorphic_base_class_from_value - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end - end - - def convert_to_id(value) - case value - when Base - value._read_attribute(primary_key) - else - value - end - end - end - end -end 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 new file mode 100644 index 0000000000..3e19646ae5 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -0,0 +1,44 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryValue # :nodoc: + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def queries + [associated_table.association_foreign_key.to_s => ids] + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :value + + private + def ids + case value + when Relation + value.select_values.empty? ? value.select(primary_key) : value + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end + end + + def primary_key + associated_table.association_primary_key + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb index 65c5159704..3bb1037885 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -9,6 +9,8 @@ module ActiveRecord predicate_builder.build(attribute, value.id) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :predicate_builder diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb deleted file mode 100644 index 0a6574fcf1..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class ClassHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - print_deprecation_warning - predicate_builder.build(attribute, value.name) - end - - protected - - attr_reader :predicate_builder - - private - - def print_deprecation_warning - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class as a value in an Active Record query is deprecated and - will be removed. Pass a string instead. - MSG - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb deleted file mode 100644 index 0c7f92b3d0..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ /dev/null @@ -1,57 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class PolymorphicArrayHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - table = value.associated_table - queries = value.type_to_ids_mapping.map do |type, ids| - { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } - end - - predicates = queries.map { |query| predicate_builder.build_from_hash(query) } - - if predicates.size > 1 - type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } - type_and_ids_predicates.inject(&:or) - else - predicates.first - end - end - - protected - - attr_reader :predicate_builder - end - - class PolymorphicArrayValue # :nodoc: - attr_reader :associated_table, :values - - def initialize(associated_table, values) - @associated_table = associated_table - @values = values - end - - def type_to_ids_mapping - default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } - end - - private - - def primary_key(value) - associated_table.association_primary_key(base_class(value)) - end - - def base_class(value) - value.class.base_class - end - - def convert_to_id(value) - value._read_attribute(primary_key(value)) - end - end - end -end 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 new file mode 100644 index 0000000000..7029ae5f47 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -0,0 +1,52 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayValue # :nodoc: + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def queries + type_to_ids_mapping.map do |type, ids| + { + associated_table.association_foreign_type.to_s => type, + associated_table.association_foreign_key.to_s => ids.size > 1 ? ids : ids.first + } + end + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :values + + private + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + case value + when Base + value.class.base_class + when Relation + value.klass.base_class + end + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key(value)) + when Relation + value.select(primary_key(value)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9fbbe32e7f..79e65baae5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -3,8 +3,6 @@ require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require "active_model/forbidden_attributes_protection" -require "active_support/core_ext/string/filters" -require "active_support/core_ext/regexp" module ActiveRecord module QueryMethods @@ -77,7 +75,7 @@ module ActiveRecord end def bound_attributes - if limit_value && !string_containing_comma?(limit_value) + if limit_value limit_bind = Attribute.with_cast_value( "LIMIT".freeze, connection.sanitize_limit(limit_value), @@ -242,8 +240,15 @@ module ActiveRecord # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - return super if block_given? - raise ArgumentError, "Call this with at least one field" if fields.empty? + if block_given? + if fields.any? + raise ArgumentError, "`select' with block doesn't take arguments." + end + + return super() + end + + raise ArgumentError, "Call `select' with at least one field" if fields.empty? spawn._select!(*fields) end @@ -651,7 +656,7 @@ module ActiveRecord end self.where_clause = self.where_clause.or(other.where_clause) - self.having_clause = self.having_clause.or(other.having_clause) + self.having_clause = having_clause.or(other.having_clause) self end @@ -682,13 +687,6 @@ module ActiveRecord end def limit!(value) # :nodoc: - if string_containing_comma?(value) - # Remove `string_containing_comma?` when removing this deprecation - ActiveSupport::Deprecation.warn(<<-WARNING.squish) - Passing a string to limit in the form "1,2" is deprecated and will be - removed in Rails 5.1. Please call `offset` explicitly instead. - WARNING - end self.limit_value = value self end @@ -755,7 +753,7 @@ module ActiveRecord # end # def none - where("1=0").extending!(NullRelation) + spawn.none! end def none! # :nodoc: @@ -840,16 +838,12 @@ module ActiveRecord def distinct(value = true) spawn.distinct!(value) end - alias uniq distinct - deprecate uniq: :distinct # Like #distinct, but modifies relation in place. def distinct!(value = true) # :nodoc: self.distinct_value = value self end - alias uniq! distinct! - deprecate uniq!: :distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -950,13 +944,7 @@ module ActiveRecord arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? - if limit_value - if string_containing_comma?(limit_value) - arel.take(connection.sanitize_limit(limit_value)) - else - arel.take(Arel::Nodes::BindParam.new) - end - end + arel.take(Arel::Nodes::BindParam.new) if limit_value arel.skip(Arel::Nodes::BindParam.new) if offset_value arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? @@ -1112,14 +1100,16 @@ module ActiveRecord end VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - "asc", "desc", "ASC", "DESC"] # :nodoc: + "asc", "desc", "ASC", "DESC"].to_set # :nodoc: def validate_order_args(args) args.each do |arg| next unless arg.is_a?(Hash) arg.each do |_key, value| - raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ - "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + unless VALID_DIRECTIONS.include?(value) + raise ArgumentError, + "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}" + end end end end @@ -1142,7 +1132,12 @@ module ActiveRecord arel_attribute(arg).asc when Hash arg.map { |field, dir| - arel_attribute(field).send(dir.downcase) + case field + when Arel::Nodes::SqlLiteral + field.send(dir.downcase) + else + arel_attribute(field).send(dir.downcase) + end } else arg @@ -1184,10 +1179,6 @@ module ActiveRecord end alias having_clause_factory where_clause_factory - def string_containing_comma?(value) - ::String === value && value.include?(",") - end - def default_value_for(name) case name when :create_with diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 190e339ea8..ada89b5ec3 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -66,7 +66,7 @@ module ActiveRecord private - def relation_with(values) # :nodoc: + def relation_with(values) result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 402f8acfd1..119910ee79 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -25,10 +25,7 @@ module ActiveRecord end def except(*columns) - WhereClause.new( - predicates_except(columns), - binds_except(columns), - ) + WhereClause.new(*except_predicates_and_binds(columns)) end def or(other) @@ -84,6 +81,8 @@ module ActiveRecord @empty ||= new([], []) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :predicates @@ -132,20 +131,35 @@ module ActiveRecord end end - def predicates_except(columns) - 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) - columns.include?(subrelation.name.to_s) + def except_predicates_and_binds(columns) + except_binds = [] + binds_index = 0 + + predicates = self.predicates.reject do |node| + except = \ + 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 + binds_contains = node.grep(Arel::Nodes::BindParam).size + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + + if except && binds_contains > 0 + (binds_index...(binds_index + binds_contains)).each do |i| + except_binds[i] = true + end end + + binds_index += binds_contains if binds_contains + + except end - end - def binds_except(columns) - binds.reject do |attr| - columns.include?(attr.name) + binds = self.binds.reject.with_index do |_, i| + except_binds[i] end + + [predicates, binds] end def predicates_with_wrapped_sql_literals diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 1e7deeffad..04bee73e8f 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -15,9 +15,12 @@ module ActiveRecord attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! - attributes, binds = predicate_builder.create_binds(attributes) - - parts = predicate_builder.build_from_hash(attributes) + if perform_case_sensitive?(options = other.last) + parts, binds = build_for_case_sensitive(attributes, options) + else + attributes, binds = predicate_builder.create_binds(attributes) + parts = predicate_builder.build_from_hash(attributes) + end when Arel::Nodes::Node parts = [opts] else @@ -27,9 +30,48 @@ module ActiveRecord WhereClause.new(parts, binds || []) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :klass, :predicate_builder + + private + + def perform_case_sensitive?(options) + options && options.key?(:case_sensitive) + end + + def build_for_case_sensitive(attributes, options) + parts, binds = [], [] + table = klass.arel_table + + attributes.each do |attribute, value| + if reflection = klass._reflect_on_association(attribute) + attribute = reflection.foreign_key.to_s + value = value[reflection.klass.primary_key] unless value.nil? + end + + if value.nil? + parts << table[attribute].eq(value) + else + column = klass.column_for_attribute(attribute) + + binds << predicate_builder.send(:build_bind_param, attribute, value) + value = Arel::Nodes::BindParam.new + + predicate = if options[:case_sensitive] + klass.connection.case_sensitive_comparison(table, attribute, column, value) + else + klass.connection.case_insensitive_comparison(table, attribute, column, value) + end + + parts << predicate + end + end + + [parts, binds] + end end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 9ed70a9c2b..26b1d48e9e 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -41,10 +41,15 @@ module ActiveRecord @column_types = column_types end + # Returns the number of elements in the rows array. def length @rows.length end + # Calls the given block once for each element in row collection, passing + # row as parameter. + # + # Returns an +Enumerator+ if no block is given. def each if block_given? hash_rows.each { |row| yield row } @@ -53,6 +58,7 @@ module ActiveRecord end end + # Returns an array of hashes representing each row record. def to_hash hash_rows end @@ -60,11 +66,12 @@ module ActiveRecord alias :map! :map alias :collect! :map - # Returns true if there are no records. + # Returns true if there are no records, otherwise false. def empty? rows.empty? end + # Returns an array of hashes representing each row record. def to_ary hash_rows end @@ -73,11 +80,15 @@ module ActiveRecord hash_rows[idx] end + # Returns the first record from the rows collection. + # If the rows collection is empty, returns +nil+. def first return nil if @rows.empty? Hash[@columns.zip(@rows.first)] end + # Returns the last record from the rows collection. + # If the rows collection is empty, returns +nil+. def last return nil if @rows.empty? Hash[@columns.zip(@rows.last)] diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index e7c0936984..64bda1539c 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,11 +1,9 @@ -require "active_support/core_ext/regexp" - module ActiveRecord module Sanitization extend ActiveSupport::Concern module ClassMethods - protected + private # Accepts an array or string of SQL conditions and sanitizes # them into a valid SQL fragment for a WHERE clause. @@ -21,7 +19,7 @@ module ActiveRecord # # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition) + def sanitize_sql_for_conditions(condition) # :doc: return nil if condition.blank? case condition @@ -47,7 +45,7 @@ module ActiveRecord # # 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 = self.table_name) + 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) @@ -63,7 +61,7 @@ module ActiveRecord # # sanitize_sql_for_order("id ASC") # # => "id ASC" - def sanitize_sql_for_order(condition) + def sanitize_sql_for_order(condition) # :doc: if condition.is_a?(Array) && condition.first.to_s.include?("?") sanitize_sql_array(condition) else @@ -86,7 +84,7 @@ module ActiveRecord # # { address: Address.new("813 abc st.", "chicago") } # # => { address_street: "813 abc st.", address_city: "chicago" } - def expand_hash_conditions_for_aggregates(attrs) + def expand_hash_conditions_for_aggregates(attrs) # :doc: expanded_attrs = {} attrs.each do |attr, value| if aggregation = reflect_on_aggregation(attr.to_sym) @@ -109,7 +107,7 @@ module ActiveRecord # # 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) + 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) @@ -131,7 +129,7 @@ module ActiveRecord # # sanitize_sql_like("snake_cased_string", "!") # # => "snake!_cased!_string" - def sanitize_sql_like(string, escape_character = "\\") + def sanitize_sql_like(string, escape_character = "\\") # :doc: pattern = Regexp.union(escape_character, "%", "_") string.gsub(pattern) { |x| [escape_character, x].join } end @@ -147,7 +145,7 @@ module ActiveRecord # # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) + 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) @@ -160,7 +158,7 @@ module ActiveRecord end end - def replace_bind_variables(statement, values) # :nodoc: + def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) bound = values.dup c = connection @@ -169,7 +167,7 @@ module ActiveRecord end end - def replace_bind_variable(value, c = connection) # :nodoc: + def replace_bind_variable(value, c = connection) if ActiveRecord::Relation === value value.to_sql else @@ -177,7 +175,7 @@ module ActiveRecord end end - def replace_named_bind_variables(statement, bind_vars) # :nodoc: + def replace_named_bind_variables(statement, bind_vars) statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| if $1 == ":" # skip postgresql casts match # return the whole match @@ -189,7 +187,7 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection) # :nodoc: + def quote_bound_value(value, c = connection) if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) @@ -201,16 +199,16 @@ module ActiveRecord end end - def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc: + def raise_if_bind_arity_mismatch(statement, expected, provided) unless expected == provided raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" end end end - # TODO: Deprecate this def quoted_id # :nodoc: self.class.connection.quote(@attributes[self.class.primary_key].value_for_database) end + deprecate :quoted_id end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 784a02d2c3..5104427339 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -40,7 +40,7 @@ module ActiveRecord # ActiveRecord::Schema.define(version: 20380119000001) do # ... # end - def self.define(info={}, &block) + def self.define(info = {}, &block) new.define(info, &block) end @@ -48,7 +48,7 @@ module ActiveRecord instance_eval(&block) if info[:version].present? - initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table connection.assume_migrated_upto_version(info[:version], migrations_paths) end @@ -61,7 +61,7 @@ module ActiveRecord # # ActiveRecord::Schema.new.migrations_paths # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths # :nodoc: + def migrations_paths ActiveRecord::Migrator.migrations_paths end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index ab2d64e903..24b81aabc8 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -11,13 +11,13 @@ module ActiveRecord ## # :singleton-method: # A list of tables which should not be dumped to the schema. - # Acceptable values are strings as well as regexp. - # This setting is only used if ActiveRecord::Base.schema_format == :ruby + # Acceptable values are strings as well as regexp if ActiveRecord::Base.schema_format == :ruby. + # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables @@ignore_tables = [] class << self - def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base) + def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) new(connection, generate_options(config)).dump(stream) stream end @@ -47,9 +47,18 @@ module ActiveRecord @options = options end - def header(stream) - define_params = @version ? "version: #{@version}" : "" + # turns 20170404131909 into "2017_04_04_131909" + def formatted_version + stringified = @version.to_s + return stringified unless stringified.length == 14 + stringified.insert(4, "_").insert(7, "_").insert(10, "_") + end + + def define_params + @version ? "version: #{formatted_version}" : "" + end + def header(stream) stream.puts <<HEADER # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to @@ -85,7 +94,7 @@ HEADER end def tables(stream) - sorted_tables = @connection.data_sources.sort - @connection.views + sorted_tables = @connection.tables.sort sorted_tables.each do |table_name| table(table_name, stream) unless ignored?(table_name) @@ -115,9 +124,7 @@ HEADER pkcol = columns.detect { |c| c.name == pk } pkcolspec = @connection.column_spec_for_primary_key(pkcol) if pkcolspec.present? - pkcolspec.each do |key, value| - tbl.print ", #{key}: #{value}" - end + tbl.print ", #{format_colspec(pkcolspec)}" end when Array tbl.print ", primary_key: #{pk.inspect}" @@ -128,26 +135,19 @@ HEADER table_options = @connection.table_options(table) if table_options.present? - table_options.each do |key, value| - tbl.print ", #{key}: #{value.inspect}" if value.present? - end + tbl.print ", #{format_options(table_options)}" end tbl.puts " do |t|" # then dump all non-primary key columns - column_specs = columns.map do |column| + columns.each do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk - @connection.column_spec(column) - end.compact - - # find all migration keys used in this table - keys = @connection.migration_keys - - column_specs.each do |colspec| - values = keys.map { |key| colspec[key] }.compact - tbl.puts " t.#{colspec[:type]} #{values.join(", ")}" + type, colspec = @connection.column_spec(column) + tbl.print " t.#{type} #{column.name.inspect}" + tbl.print ", #{format_colspec(colspec)}" if colspec.present? + tbl.puts end indexes_in_create(table, tbl) @@ -171,7 +171,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| table_name = remove_prefix_and_suffix(index.table).inspect - " add_index #{([table_name]+index_parts(index)).join(', ')}" + " add_index #{([table_name] + index_parts(index)).join(', ')}" end stream.puts add_index_statements.sort.join("\n") @@ -194,14 +194,10 @@ HEADER "name: #{index.name.inspect}", ] index_parts << "unique: true" if index.unique - - index_lengths = (index.lengths || []).compact - index_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? - - index_orders = index.orders || {} - index_parts << "order: #{index.orders.inspect}" if index_orders.any? + index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? + index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? index_parts << "where: #{index.where.inspect}" if index.where - index_parts << "using: #{index.using.inspect}" if index.using + index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type index_parts << "comment: #{index.comment.inspect}" if index.comment index_parts @@ -237,6 +233,14 @@ HEADER end end + def format_colspec(colspec) + colspec.map { |key, value| "#{key}: #{value}" }.join(", ") + end + + def format_options(options) + options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 99b23e5593..f59737afb0 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -17,7 +17,7 @@ module ActiveRecord end def table_exists? - ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } + connection.table_exists?(table_name) end def create_table @@ -39,7 +39,11 @@ module ActiveRecord end def normalized_versions - pluck(:version).map { |v| normalize_migration_number v } + all_versions.map { |v| normalize_migration_number v } + end + + def all_versions + order(:version).pluck(:version) end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index d1bd1cd89a..7c00e7e4ed 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -33,7 +33,7 @@ module ActiveRecord def populate_with_current_scope_attributes # :nodoc: return unless self.class.scope_attributes? - self.class.scope_attributes.each do |att,value| + self.class.scope_attributes.each do |att, value| send("#{att}=", value) if respond_to?("#{att}=") end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 9d8253faa3..ba5cc29ac1 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,11 +5,8 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false, instance_predicate: false - class_attribute :default_scope_override, instance_writer: false, instance_predicate: false - - self.default_scopes = [] - self.default_scope_override = nil + class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: [] + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil end module ClassMethods @@ -44,7 +41,7 @@ module ActiveRecord self.current_scope = nil end - protected + private # Use this macro in your model to set a default scope for all operations on # the model. @@ -87,7 +84,7 @@ module ActiveRecord # # Should return a scope, you can call 'super' here etc. # end # end - def default_scope(scope = nil) + def default_scope(scope = nil) # :doc: scope = Proc.new if block_given? if scope.is_a?(Relation) || !scope.respond_to?(:call) @@ -101,7 +98,7 @@ module ActiveRecord self.default_scopes += [scope] end - def build_default_scope(base_rel = nil) # :nodoc: + def build_default_scope(base_rel = nil) return if abstract_class? if default_scope_override.nil? @@ -122,18 +119,18 @@ module ActiveRecord end end - def ignore_default_scope? # :nodoc: + def ignore_default_scope? ScopeRegistry.value_for(:ignore_default_scope, base_class) end - def ignore_default_scope=(ignore) # :nodoc: + def ignore_default_scope=(ignore) ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore) end # The ignore_default_scope flag is used to prevent an infinite recursion # situation where a default scope references a scope which has a default # scope which references a scope... - def evaluate_default_scope # :nodoc: + def evaluate_default_scope return if ignore_default_scope? begin diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 094c0e9c6f..a61fdd6454 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,19 +30,22 @@ module ActiveRecord end def default_scoped # :nodoc: - scope = build_default_scope + scope = relation + build_default_scope(scope) || scope + end - if scope - relation.spawn.merge!(scope) + def default_extensions # :nodoc: + if scope = current_scope || build_default_scope + scope.extensions else - relation + [] end end # Adds a class method for retrieving and querying objects. # The method is intended to return an ActiveRecord::Relation # object, which is composable with other scopes. - # If it returns nil or false, an + # If it returns +nil+ or +false+, an # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead. # # A \scope represents a narrowing of a database query, such as @@ -156,29 +159,29 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { instance_exec(*args, &body) } + scope = all + scope = scope.scoping { instance_exec(*args, &body) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end else singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } + scope = all + scope = scope.scoping { body.call(*args) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end end end - protected + private - def valid_scope_name?(name) - if respond_to?(name, true) && logger - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." + def valid_scope_name?(name) + if respond_to?(name, true) && logger + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end end - end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 7606961e2e..115799cc20 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -27,7 +27,7 @@ module ActiveRecord # Load securerandom only when has_secure_token is used. require "active_support/core_ext/securerandom" define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } - before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?") } + before_create { send("#{attribute}=", self.class.generate_unique_secure_token) unless send("#{attribute}?") } end def generate_unique_secure_token diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 5a408e7b8e..db2bd0b55e 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -9,7 +9,7 @@ module ActiveRecord #:nodoc: end def serializable_hash(options = nil) - options = options.try(:clone) || {} + options = options.try(:dup) || {} options[:except] = Array(options[:except]).map(&:to_s) options[:except] |= Array(self.class.inheritance_column) diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 691940ab70..1877489e55 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -41,7 +41,7 @@ module ActiveRecord class PartialQuery < Query # :nodoc: def initialize(values) @values = values - @indexes = values.each_with_index.find_all { |thing,i| + @indexes = values.each_with_index.find_all { |thing, i| Arel::Nodes::BindParam === thing }.map(&:last) end @@ -68,7 +68,7 @@ module ActiveRecord class BindMap # :nodoc: def initialize(bound_attributes) - @indexes = [] + @indexes = [] @bound_attributes = bound_attributes bound_attributes.each_with_index do |attr, i| @@ -80,7 +80,7 @@ module ActiveRecord def bind(values) bas = @bound_attributes.dup - @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) } + @indexes.each_with_index { |offset, i| bas[offset] = bas[offset].with_cast_value(values[i]) } bas end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 066573192e..006afe7495 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -78,7 +78,7 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, IndifferentCoder.new(options[:coder]) + serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end @@ -121,18 +121,17 @@ module ActiveRecord end end - protected - def read_store_attribute(store_attribute, key) + private + def read_store_attribute(store_attribute, key) # :doc: accessor = store_accessor_for(store_attribute) accessor.read(self, store_attribute, key) end - def write_store_attribute(store_attribute, key, value) + def write_store_attribute(store_attribute, key, value) # :doc: accessor = store_accessor_for(store_attribute) accessor.write(self, store_attribute, key, value) end - private def store_accessor_for(store_attribute) type_for_attribute(store_attribute.to_s).accessor end @@ -178,12 +177,12 @@ module ActiveRecord end class IndifferentCoder # :nodoc: - def initialize(coder_or_class_name) + def initialize(attr_name, coder_or_class_name) @coder = if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) coder_or_class_name else - ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) + ActiveRecord::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object) end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 58184f3872..71efc1829a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -44,7 +44,7 @@ module ActiveRecord end def associated_table(table_name) - association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) + association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize) if !association && table_name == arel_table.name return self @@ -64,6 +64,8 @@ module ActiveRecord association && association.polymorphic? end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :klass, :arel_table, :association diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index a19913f2a8..ba686fc562 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/filters" - module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -35,12 +33,22 @@ module ActiveRecord # # DatabaseTasks.create_current('production') module DatabaseTasks + ## + # :singleton-method: + # Extra flags passed to database CLI tool (mysqldump/pg_dump) when calling db:structure:dump + mattr_accessor :structure_dump_flags, instance_accessor: false + + ## + # :singleton-method: + # Extra flags passed to database CLI tool when calling db:structure:load + mattr_accessor :structure_load_flags, instance_accessor: false + extend self attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader attr_accessor :database_configuration - LOCAL_HOSTS = ["127.0.0.1", "localhost"] + LOCAL_HOSTS = ["127.0.0.1", "localhost"] def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] @@ -63,9 +71,9 @@ module ActiveRecord @tasks[pattern] = task end - register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) - register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) - register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + register_task(/mysql/, "ActiveRecord::Tasks::MySQLDatabaseTasks") + register_task(/postgresql/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks") + register_task(/sqlite/, "ActiveRecord::Tasks::SQLiteDatabaseTasks") def db_dir @db_dir ||= Rails.application.config.paths["db"].first @@ -154,9 +162,11 @@ module ActiveRecord end def migrate - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? + + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - scope = ENV["SCOPE"] + scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose Migrator.migrate(migrations_paths, version) do |migration| scope.blank? || scope == migration.scope @@ -204,13 +214,13 @@ module ActiveRecord def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 - class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename) + class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename, structure_dump_flags) end def structure_load(*arguments) configuration = arguments.first filename = arguments.delete_at 1 - class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename) + class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags) end def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: @@ -231,14 +241,6 @@ module ActiveRecord ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end - def load_schema_for(*args) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - This method was renamed to `#load_schema` and will be removed in the future. - Use `#load_schema` instead. - MSG - load_schema(*args) - end - def schema_file(format = ActiveRecord::Base.schema_format) case format when :ruby @@ -267,20 +269,30 @@ module ActiveRecord if seed_loader seed_loader.load_seed else - raise "You tried to load seed data, but no seed loader is specified. Please specify seed " + - "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" + + raise "You tried to load seed data, but no seed loader is specified. Please specify seed " \ + "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" \ "Seed loader should respond to load_seed method" end end + # Dumps the schema cache in YAML format for the connection into the file + # + # ==== Examples: + # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, "tmp/schema_dump.yaml") + def dump_schema_cache(conn, filename) + conn.schema_cache.clear! + conn.data_sources.each { |table| conn.schema_cache.add(table) } + open(filename, "wb") { |f| f.write(YAML.dump(conn.schema_cache)) } + end + private def class_for_adapter(adapter) - key = @tasks.keys.detect { |pattern| adapter[pattern] } - unless key + _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } + unless task raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" end - @tasks[key] + task.is_a?(String) ? task.constantize : task end def each_current_configuration(environment) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 3a5e0b8dfe..541165b3d1 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -14,7 +14,7 @@ module ActiveRecord connection.create_database configuration["database"], creation_options establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database exists/ === error.message + if error.message.include?("database exists") raise DatabaseAlreadyExists else raise @@ -53,21 +53,29 @@ module ActiveRecord connection.collation end - def structure_dump(filename) + def structure_dump(filename, extra_flags) args = prepare_command_options args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["--routines"]) args.concat(["--skip-comments"]) + args.concat(Array(extra_flags)) if extra_flags + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" } + end + args.concat(["#{configuration['database']}"]) run_cmd("mysqldump", args, "dumping") end - def structure_load(filename) + def structure_load(filename, extra_flags) args = prepare_command_options args.concat(["--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) args.concat(["--database", "#{configuration['database']}"]) + args.concat(Array(extra_flags)) if extra_flags run_cmd("mysql", args, "loading") end @@ -102,7 +110,7 @@ module ActiveRecord def grant_statement <<-SQL -GRANT ALL PRIVILEGES ON #{configuration['database']}.* +GRANT ALL PRIVILEGES ON `#{configuration['database']}`.* TO '#{configuration['username']}'@'localhost' IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; SQL diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index a3a9430c03..7f1a768d8b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,8 +1,11 @@ +require "tempfile" + module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: DEFAULT_ENCODING = ENV["CHARSET"] || "utf8" ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze + SQL_COMMENT_BEGIN = "--".freeze delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -17,7 +20,7 @@ module ActiveRecord configuration.merge("encoding" => encoding) establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database .* already exists/ === error.message + if /database .* already exists/.match?(error.message) raise DatabaseAlreadyExists else raise @@ -43,7 +46,7 @@ module ActiveRecord create true end - def structure_dump(filename) + def structure_dump(filename, extra_flags) set_psql_env search_path = \ @@ -57,20 +60,30 @@ module ActiveRecord end args = ["-s", "-x", "-O", "-f", filename] + args.concat(Array(extra_flags)) if extra_flags unless search_path.blank? args += search_path.split(",").map do |part| "--schema=#{part.strip}" end end + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.flat_map { |table| ["-T", table] } + end + args << configuration["database"] run_cmd("pg_dump", args, "dumping") + remove_sql_header_comments(filename) File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end - def structure_load(filename) + def structure_load(filename, extra_flags) set_psql_env - args = [ "-v", ON_ERROR_STOP_1, "-q", "-f", filename, configuration["database"] ] - run_cmd("psql", args, "loading" ) + args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + run_cmd("psql", args, "loading") end private @@ -107,6 +120,22 @@ module ActiveRecord msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg end + + def remove_sql_header_comments(filename) + removing_comments = true + tempfile = Tempfile.open("uncommented_structure.sql") + begin + File.foreach(filename) do |line| + unless removing_comments && (line.start_with?(SQL_COMMENT_BEGIN) || line.blank?) + tempfile << line + removing_comments = false + end + end + ensure + tempfile.close + end + FileUtils.mv(tempfile.path, filename) + end end end end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 31f1b7efd4..7043d2f0c8 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -21,7 +21,7 @@ module ActiveRecord FileUtils.rm(file) rescue Errno::ENOENT => error - raise NoDatabaseError.new(error.message, error) + raise NoDatabaseError.new(error.message) end def purge @@ -35,14 +35,25 @@ module ActiveRecord connection.encoding end - def structure_dump(filename) - dbfile = configuration["database"] - `sqlite3 #{dbfile} .schema > #{filename}` + def structure_dump(filename, extra_flags) + args = [] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + condition = ignore_tables.map { |table| connection.quote_table_name(table) }.join(", ") + args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name" + else + args << ".schema" + end + run_cmd("sqlite3", args, filename) end - def structure_load(filename) + def structure_load(filename, extra_flags) dbfile = configuration["database"] - `sqlite3 #{dbfile} < "#{filename}"` + flags = extra_flags.join(" ") if extra_flags + `sqlite3 #{flags} #{dbfile} < "#{filename}"` end private @@ -54,6 +65,17 @@ module ActiveRecord def root @root end + + def run_cmd(cmd, args, out) + fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) + end + + def run_cmd_error(cmd, args) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 6641ab5df1..55f3a194a9 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveRecord # = Active Record \Timestamp # @@ -42,8 +43,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps - self.record_timestamps = true + class_attribute :record_timestamps, default: true end def initialize_dup(other) # :nodoc: @@ -51,15 +51,41 @@ module ActiveRecord clear_timestamp_attributes end + class_methods do + private + def timestamp_attributes_for_create_in_model + timestamp_attributes_for_create.select { |c| column_names.include?(c) } + end + + def timestamp_attributes_for_update_in_model + timestamp_attributes_for_update.select { |c| column_names.include?(c) } + end + + def all_timestamp_attributes_in_model + timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model + end + + def timestamp_attributes_for_create + ["created_at", "created_on"] + end + + def timestamp_attributes_for_update + ["updated_at", "updated_on"] + end + + def current_time_from_proper_timezone + default_timezone == :utc ? Time.now.utc : Time.now + end + end + private def _create_record if record_timestamps current_time = current_time_from_proper_timezone - all_timestamp_attributes.each do |column| - column = column.to_s - if has_attribute?(column) && !attribute_present?(column) + all_timestamp_attributes_in_model.each do |column| + if !attribute_present?(column) write_attribute(column, current_time) end end @@ -73,8 +99,7 @@ module ActiveRecord current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| - column = column.to_s - next if attribute_changed?(column) + next if will_save_change_to_attribute?(column) write_attribute(column, current_time) end end @@ -82,34 +107,26 @@ module ActiveRecord end def should_record_timestamps? - record_timestamps && (!partial_writes? || changed?) + record_timestamps && (!partial_writes? || has_changes_to_save?) end def timestamp_attributes_for_create_in_model - timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) } + self.class.send(:timestamp_attributes_for_create_in_model) end def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } + self.class.send(:timestamp_attributes_for_update_in_model) end def all_timestamp_attributes_in_model - timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model - end - - def timestamp_attributes_for_update - [:updated_at, :updated_on] - end - - def timestamp_attributes_for_create - [:created_at, :created_on] + self.class.send(:all_timestamp_attributes_in_model) end - def all_timestamp_attributes - timestamp_attributes_for_create + timestamp_attributes_for_update + def current_time_from_proper_timezone + self.class.send(:current_time_from_proper_timezone) end - def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) + def max_updated_column_timestamp(timestamp_names = self.class.send(:timestamp_attributes_for_update)) timestamp_names .map { |attr| self[attr] } .compact @@ -117,10 +134,6 @@ module ActiveRecord .max end - def current_time_from_proper_timezone - self.class.default_timezone == :utc ? Time.now.utc : Time.now - end - # Clear attributes and changed_attributes def clear_timestamp_attributes all_timestamp_attributes_in_model.each do |attribute_name| diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index c337a7532f..cacde9c881 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -25,7 +25,7 @@ module ActiveRecord # touch the parents as we are not calling the after_save callbacks self.class.reflect_on_all_associations(:belongs_to).each do |r| if touch = r.options[:touch] - ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, r.foreign_key, r.name, touch, :touch_later) + ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, changes_to_save, r.foreign_key, r.name, touch, :touch_later) end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index af3fc88282..45795fa287 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,7 +11,6 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, - terminator: deprecated_false_terminator, scope: [:kind, :name] end @@ -124,7 +123,7 @@ module ActiveRecord # # statement will cause a PostgreSQL error, even though the unique # # constraint is no longer violated: # Number.create(i: 1) - # # => "PGError: ERROR: current transaction is aborted, commands + # # => "PG::Error: ERROR: current transaction is aborted, commands # # ignored until end of transaction block" # end # @@ -274,16 +273,6 @@ module ActiveRecord set_callback(:rollback_without_transaction_enrollment, :after, *args, &block) end - def raise_in_transactional_callbacks - ActiveSupport::Deprecation.warn("ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.") - true - end - - def raise_in_transactional_callbacks=(value) - ActiveSupport::Deprecation.warn("ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.") - value - end - private def set_options_for_callbacks!(args, enforced_options = {}) @@ -294,7 +283,7 @@ module ActiveRecord fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) - options[:if] << "transaction_include_any_action?(#{fire_on})" + options[:if].unshift("transaction_include_any_action?(#{fire_on})") end end @@ -407,10 +396,10 @@ module ActiveRecord end end - protected + private # Save the new record state and id of a record so it can be restored later if a transaction fails. - def remember_transaction_record_state #:nodoc: + def remember_transaction_record_state @_start_transaction_state[:id] = id @_start_transaction_state.reverse_merge!( new_record: @new_record, @@ -421,18 +410,18 @@ module ActiveRecord end # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc: + def clear_transaction_record_state @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 end # Force to clear the transaction record state. - def force_clear_transaction_record_state #:nodoc: + def force_clear_transaction_record_state @_start_transaction_state.clear end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc: + def restore_transaction_record_state(force = false) unless @_start_transaction_state.empty? transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force @@ -450,31 +439,30 @@ module ActiveRecord end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc: + def transaction_record_state(state) @_start_transaction_state[state] end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_any_action?(actions) #:nodoc: + def transaction_include_any_action?(actions) actions.any? do |action| case action when :create transaction_record_state(:new_record) when :destroy - destroyed? + defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback when :update - !(transaction_record_state(:new_record) || destroyed?) + !(transaction_record_state(:new_record) || destroyed?) && + (defined?(@_trigger_update_callback) && @_trigger_update_callback) end end end - private - - def set_transaction_state(state) # :nodoc: + def set_transaction_state(state) @transaction_state = state end - def has_transactional_callbacks? # :nodoc: + def has_transactional_callbacks? !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 0b48d2186a..4f632660a8 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -5,7 +5,10 @@ require "active_record/type/internal/timezone" require "active_record/type/date" require "active_record/type/date_time" +require "active_record/type/decimal_without_scale" require "active_record/type/time" +require "active_record/type/text" +require "active_record/type/unsigned_integer" require "active_record/type/serialized" require "active_record/type/adapter_specific_registry" @@ -53,12 +56,9 @@ module ActiveRecord Binary = ActiveModel::Type::Binary Boolean = ActiveModel::Type::Boolean Decimal = ActiveModel::Type::Decimal - DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale Float = ActiveModel::Type::Float Integer = ActiveModel::Type::Integer String = ActiveModel::Type::String - Text = ActiveModel::Type::Text - UnsignedInteger = ActiveModel::Type::UnsignedInteger Value = ActiveModel::Type::Value register(:big_integer, Type::BigInteger, override: false) diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index d0f9581576..7cc866f7a7 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -50,6 +50,8 @@ module ActiveRecord priority <=> other.priority end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :block, :adapter, :override @@ -110,6 +112,8 @@ module ActiveRecord super | 4 end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :options, :klass diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb new file mode 100644 index 0000000000..53a5e205da --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Type + class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc: + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s.inspect + end + end + end +end diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index e19c5a14da..a8d6a63465 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -24,6 +24,10 @@ module ActiveRecord end end + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + def accessor ActiveRecord::Store::StringKeyedHashAccessor end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ac9134bfcb..edbd20a6c1 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,6 +1,8 @@ module ActiveRecord module Type class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder @@ -43,7 +45,7 @@ module ActiveRecord def assert_valid_value(value) if coder.respond_to?(:assert_valid_value) - coder.assert_valid_value(value) + coder.assert_valid_value(value, action: "serialize") end end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..cb1949700a --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,9 @@ +module ActiveRecord + module Type + class Text < ActiveModel::Type::String # :nodoc: + def type + :text + end + end + end +end diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb index 288fa23efe..9ae0109f9f 100644 --- a/activemodel/lib/active_model/type/unsigned_integer.rb +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -1,6 +1,6 @@ -module ActiveModel +module ActiveRecord module Type - class UnsignedInteger < Integer # :nodoc: + class UnsignedInteger < ActiveModel::Type::Integer # :nodoc: private def max_value diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 6c54792e26..9f7bbe8843 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -12,6 +12,8 @@ module ActiveRecord connection.type_cast_from_column(column, value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :table_name diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 52529a6b42..9f79723125 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -11,6 +11,8 @@ module ActiveRecord type.serialize(value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :types diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index ecaf04e39e..9633f226f0 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -40,13 +40,13 @@ module ActiveRecord # The validation process on save can be skipped by passing <tt>validate: false</tt>. # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced # with this when the validations module is mixed in, which it is by default. - def save(options={}) + def save(options = {}) perform_validations(options) ? super : false end # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. - def save!(options={}) + def save!(options = {}) perform_validations(options) ? super : raise_validation_error end @@ -68,7 +68,7 @@ module ActiveRecord alias_method :validate, :valid? - protected + private def default_validation_context new_record? ? :create : :update @@ -78,7 +78,7 @@ module ActiveRecord raise(RecordInvalid.new(self)) end - def perform_validations(options={}) # :nodoc: + def perform_validations(options = {}) options[:validate] == false || valid?(options[:context]) end end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b14db85167..c695965d7b 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -37,7 +37,7 @@ module ActiveRecord # # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index ad82ea66c4..7cfd55f516 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -44,7 +44,7 @@ module ActiveRecord # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) @@ -57,7 +57,7 @@ module ActiveRecord # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See ActiveModel::Validation#validates! for more information. + # See ActiveModel::Validations#validates! for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 8c4930a81d..154cf5f1a4 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -17,7 +17,7 @@ module ActiveRecord relation = build_relation(finder_class, attribute, value) if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id_was || record.id) + relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end @@ -33,13 +33,13 @@ module ActiveRecord end end - protected + private # The check for an existing value should be run from a class that # isn't abstract. This means working down from the current class # (self), to the first non-abstract class. Since classes don't know # their subclasses, we have to build the hierarchy between self and # the record's class. - def find_finder_class_for(record) #:nodoc: + def find_finder_class_for(record) class_hierarchy = [record.class] while class_hierarchy.first != @klass @@ -49,47 +49,16 @@ module ActiveRecord class_hierarchy.detect { |klass| !klass.abstract_class? } end - def build_relation(klass, attribute, value) # :nodoc: - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key - value = value.attributes[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - return klass.unscoped.where!(attribute => value) - end - - # the attribute may be an aliased attribute - if klass.attribute_alias?(attribute) - attribute = klass.attribute_alias(attribute) - end - - attribute_name = attribute.to_s - - table = klass.arel_table - column = klass.columns_hash[attribute_name] - cast_type = klass.type_for_attribute(attribute_name) - - comparison = if !options[:case_sensitive] - # will use SQL LOWER function before comparison, unless it detects a case insensitive collation - klass.connection.case_insensitive_comparison(table, attribute, column, value) - else - klass.connection.case_sensitive_comparison(table, attribute, column, value) - end - klass.unscoped.tap do |scope| - parts = [comparison] - binds = [Relation::QueryAttribute.new(attribute_name, value, cast_type)] - scope.where_clause += Relation::WhereClause.new(parts, binds) - end + def build_relation(klass, attribute, value) + klass.unscoped.where!({ attribute => value }, options) end def scope_relation(record, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class._reflect_on_association(scope_item) - scope_value = record.send(reflection.foreign_key) - scope_item = reflection.foreign_key + scope_value = if record.class._reflect_on_association(scope_item) + record.association(scope_item).reader else - scope_value = record._read_attribute(scope_item) + record._read_attribute(scope_item) end relation = relation.where(scope_item => scope_value) end diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index 68fca44e3b..a79b8eafea 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -10,7 +10,7 @@ module ActiveRecord # Set the current directory as base for the inherited generators. def self.base_root - File.dirname(__FILE__) + __dir__ end end end diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4263c11ffc..47c0981a49 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -20,6 +20,14 @@ module ActiveRecord key_type = options[:primary_key_type] ", id: :#{key_type}" if key_type end + + def db_migrate_path + if defined?(Rails.application) && Rails.application + Rails.application.config.paths["db/migrate"].to_ary.first + else + "db/migrate" + end + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 76ed25ea75..1f1c47499b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -1,5 +1,4 @@ require "rails/generators/active_record" -require "active_support/core_ext/regexp" module ActiveRecord module Generators # :nodoc: @@ -11,12 +10,16 @@ module ActiveRecord def create_migration_file set_local_assigns! validate_file_name! - migration_template @migration_template, "db/migrate/#{file_name}.rb" + migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb") end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :migration_action, :join_tables + private + # Sets the default migration template that is being used for the generation of the migration. # Depending on command line arguments, the migration template and the table name instance # variables are set up. @@ -53,7 +56,6 @@ module ActiveRecord end.to_sym end - private def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index f1ddc61688..5cec07d2e3 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -17,7 +17,7 @@ module ActiveRecord def create_migration_file return unless options[:migration] && options[:parent].nil? attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false - migration_template "../../migration/templates/create_table_migration.rb", "db/migrate/create_#{table_name}.rb" + migration_template "../../migration/templates/create_table_migration.rb", File.join(db_migrate_path, "create_#{table_name}.rb") end def create_model_file @@ -33,7 +33,7 @@ module ActiveRecord hook_for :test_framework - protected + private def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } @@ -41,7 +41,7 @@ module ActiveRecord # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. def generate_application_record - if self.behavior == :invoke && !application_record_exist? + if behavior == :invoke && !application_record_exist? template "application_record.rb", application_record_file_name end end diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 43c817e057..b0d8050721 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -9,7 +9,7 @@ module ActiveRecord class FakeAdapter < AbstractAdapter attr_accessor :data_sources, :primary_keys - @columns = Hash.new { |h,k| h[k] = [] } + @columns = Hash.new { |h, k| h[k] = [] } class << self attr_reader :columns end diff --git a/activerecord/test/assets/schema_dump_5_1.yml b/activerecord/test/assets/schema_dump_5_1.yml new file mode 100644 index 0000000000..f37977daf2 --- /dev/null +++ b/activerecord/test/assets/schema_dump_5_1.yml @@ -0,0 +1,345 @@ +--- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache +columns: + posts: + - &1 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: id + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: INTEGER + type: :integer + limit: + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - &2 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: author_id + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: + default_function: + collation: + comment: + - &3 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: title + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: varchar + type: :string + limit: + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - &4 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: body + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: text + type: :text + limit: + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - &5 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: type + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: varchar + type: :string + limit: + precision: + scale: + 'null': true + default: + default_function: + collation: + comment: + - &6 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: comments_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &7 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: taggings_with_delete_all_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &8 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: taggings_with_destroy_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &9 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: tags_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &10 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: tags_with_destroy_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &11 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: tags_with_nullify_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: +columns_hash: + posts: + id: *1 + author_id: *2 + title: *3 + body: *4 + type: *5 + comments_count: *6 + taggings_with_delete_all_count: *7 + taggings_with_destroy_count: *8 + tags_count: *9 + tags_with_destroy_count: *10 + tags_with_nullify_count: *11 +primary_keys: + posts: id +data_sources: + ar_internal_metadata: true + table_with_autoincrement: true + accounts: true + admin_accounts: true + admin_users: true + aircraft: true + articles: true + articles_magazines: true + articles_tags: true + audit_logs: true + authors: true + author_addresses: true + author_favorites: true + auto_id_tests: true + binaries: true + birds: true + books: true + booleans: true + bulbs: true + CamelCase: true + cars: true + carriers: true + categories: true + categories_posts: true + categorizations: true + citations: true + clubs: true + collections: true + colnametests: true + columns: true + comments: true + companies: true + content: true + content_positions: true + vegetables: true + computers: true + computers_developers: true + contracts: true + customers: true + customer_carriers: true + dashboards: true + developers: true + developers_projects: true + dog_lovers: true + dogs: true + doubloons: true + edges: true + engines: true + entrants: true + essays: true + events: true + eyes: true + funny_jokes: true + cold_jokes: true + friendships: true + goofy_string_id: true + having: true + guids: true + guitars: true + inept_wizards: true + integer_limits: true + invoices: true + iris: true + items: true + jobs: true + jobs_pool: true + keyboards: true + legacy_things: true + lessons: true + lessons_students: true + students: true + lint_models: true + line_items: true + lions: true + lock_without_defaults: true + lock_without_defaults_cust: true + magazines: true + mateys: true + members: true + member_details: true + member_friends: true + memberships: true + member_types: true + mentors: true + minivans: true + minimalistics: true + mixed_case_monkeys: true + mixins: true + movies: true + notifications: true + numeric_data: true + orders: true + organizations: true + owners: true + paint_colors: true + paint_textures: true + parrots: true + parrots_pirates: true + parrots_treasures: true + people: true + peoples_treasures: true + personal_legacy_things: true + pets: true + pets_treasures: true + pirates: true + posts: true + serialized_posts: true + images: true + price_estimates: true + products: true + product_types: true + projects: true + randomly_named_table1: true + randomly_named_table2: true + randomly_named_table3: true + ratings: true + readers: true + references: true + shape_expressions: true + ships: true + ship_parts: true + prisoners: true + shop_accounts: true + speedometers: true + sponsors: true + string_key_objects: true + subscribers: true + subscriptions: true + tags: true + taggings: true + tasks: true + topics: true + toys: true + traffic_lights: true + treasures: true + tuning_pegs: true + tyres: true + variants: true + vertices: true + warehouse-things: true + circles: true + squares: true + triangles: true + non_poly_ones: true + non_poly_twos: true + men: true + faces: true + interests: true + zines: true + wheels: true + countries: true + treaties: true + countries_treaties: true + liquid: true + molecules: true + electrons: true + weirds: true + nodes: true + trees: true + hotels: true + departments: true + cake_designers: true + drink_designers: true + chefs: true + recipes: true + records: true + overloaded_types: true + users: true + test_with_keyword_column_name: true + fk_test_has_pk: true + fk_test_has_fk: true +version: 0 diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 8fa0645b0f..a1fb6427f9 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -30,9 +30,18 @@ module ActiveRecord assert_nothing_raised { Book.destroy(0) } end + def test_valid_column + @connection.native_database_types.each_key do |type| + assert @connection.valid_type?(type) + end + end + + def test_invalid_column + assert_not @connection.valid_type?(:foobar) + end + def test_tables - tables = nil - ActiveSupport::Deprecation.silence { tables = @connection.tables } + tables = @connection.tables assert_includes tables, "accounts" assert_includes tables, "authors" assert_includes tables, "tasks" @@ -40,15 +49,11 @@ module ActiveRecord end def test_table_exists? - ActiveSupport::Deprecation.silence do - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) - end - end - - def test_table_exists_checking_both_tables_and_views_is_deprecated - assert_deprecated { @connection.table_exists?("accounts") } + assert @connection.table_exists?("accounts") + assert @connection.table_exists?(:accounts) + assert_not @connection.table_exists?("nonexistingtable") + assert_not @connection.table_exists?("'") + assert_not @connection.table_exists?(nil) end def test_data_sources @@ -63,26 +68,22 @@ module ActiveRecord assert @connection.data_source_exists?("accounts") assert @connection.data_source_exists?(:accounts) assert_not @connection.data_source_exists?("nonexistingtable") + assert_not @connection.data_source_exists?("'") assert_not @connection.data_source_exists?(nil) end def test_indexes idx_name = "accounts_idx" - if @connection.respond_to?(:indexes) - indexes = @connection.indexes("accounts") - assert indexes.empty? - - @connection.add_index :accounts, :firm_id, name: idx_name - indexes = @connection.indexes("accounts") - assert_equal "accounts", indexes.first.table - assert_equal idx_name, indexes.first.name - assert !indexes.first.unique - assert_equal ["firm_id"], indexes.first.columns - else - warn "#{@connection.class} does not respond to #indexes" - end + indexes = @connection.indexes("accounts") + assert indexes.empty? + @connection.add_index :accounts, :firm_id, name: idx_name + indexes = @connection.indexes("accounts") + assert_equal "accounts", indexes.first.table + assert_equal idx_name, indexes.first.name + assert !indexes.first.unique + assert_equal ["firm_id"], indexes.first.columns ensure @connection.remove_index(:accounts, name: idx_name) rescue nil end @@ -184,35 +185,15 @@ module ActiveRecord assert_not_nil error.cause end - unless current_adapter?(:SQLite3Adapter) - def test_foreign_key_violations_are_translated_to_specific_exception - error = assert_raises(ActiveRecord::InvalidForeignKey) do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" - end - end - - assert_not_nil error.cause + def test_not_null_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::NotNullViolation) do + Post.create end - def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false - klass_has_fk = Class.new(ActiveRecord::Base) do - self.table_name = "fk_test_has_fk" - end - - error = assert_raises(ActiveRecord::InvalidForeignKey) do - has_fk = klass_has_fk.new - has_fk.fk_id = 1231231231 - has_fk.save(validate: false) - end - - assert_not_nil error.cause - end + assert_not_nil error.cause + end + unless current_adapter?(:SQLite3Adapter) def test_value_limit_violations_are_translated_to_specific_exception error = assert_raises(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") @@ -220,22 +201,13 @@ module ActiveRecord assert_not_nil error.cause end - end - def test_disable_referential_integrity - assert_nothing_raised do - @connection.disable_referential_integrity do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" - end - # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block - # and will fail (at least on Oracle) - @connection.execute "DELETE FROM fk_test_has_fk" + def test_numeric_value_out_of_ranges_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::RangeError) do + Book.connection.create("INSERT INTO books(author_id) VALUES (9223372036854775808)") end + + assert_not_nil error.cause end end @@ -244,6 +216,15 @@ module ActiveRecord assert result.is_a?(ActiveRecord::Result) end + if ActiveRecord::Base.connection.prepared_statements + def test_select_all_with_legacy_binds + post = Post.create!(title: "foo", body: "bar") + expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}") + result = @connection.select_all("SELECT * FROM posts WHERE id = #{bind_param.to_sql}", nil, [[nil, post.id]]) + assert_equal expected.to_hash, result.to_hash + end + end + def test_select_methods_passing_a_association_relation author = Author.create!(name: "john") Post.create!(author: author, title: "foo", body: "bar") @@ -271,25 +252,68 @@ module ActiveRecord unless current_adapter?(:PostgreSQLAdapter) def test_log_invalid_encoding - error = assert_raise ActiveRecord::StatementInvalid do + error = assert_raises RuntimeError do @connection.send :log, "SELECT 'ы' FROM DUAL" do raise "ы".force_encoding(Encoding::ASCII_8BIT) end end - assert_not_nil error.cause + assert_not_nil error.message + end + end + end + + class AdapterForeignKeyTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + end + + def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false + klass_has_fk = Class.new(ActiveRecord::Base) do + self.table_name = "fk_test_has_fk" + end + + error = assert_raises(ActiveRecord::InvalidForeignKey) do + has_fk = klass_has_fk.new + has_fk.fk_id = 1231231231 + has_fk.save(validate: false) end + + assert_not_nil error.cause end - if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) - def test_tables_returning_both_tables_and_views_is_deprecated - assert_deprecated { @connection.tables } + def test_foreign_key_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::InvalidForeignKey) do + insert_into_fk_test_has_fk end + + assert_not_nil error.cause end - def test_passing_arguments_to_tables_is_deprecated - assert_deprecated { @connection.tables(:books) } + def test_disable_referential_integrity + assert_nothing_raised do + @connection.disable_referential_integrity do + insert_into_fk_test_has_fk + # should delete created record as otherwise disable_referential_integrity will try to enable constraints + # after executed block and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" + end + end end + + private + + def insert_into_fk_test_has_fk + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index a70eb5a094..67e1efde27 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -28,12 +28,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " assert_equal expected, add_index(:people, [:last_name, :first_name], length: 15) + assert_equal expected, add_index(:people, ["last_name", "first_name"], length: 15) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15 }) + assert_equal expected, add_index(:people, ["last_name", "first_name"], length: { last_name: 15 }) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15, first_name: 10 }) + assert_equal expected, add_index(:people, ["last_name", :first_name], length: { last_name: 15, "first_name" => 10 }) %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -89,8 +92,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase assert_equal expected, actual end - expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" - actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| t.index :last_name, length: 10, using: :btree, algorithm: :copy end assert_equal expected, actual diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 2fa39282fb..58698d59db 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -38,7 +38,7 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal :string, string_column.type end - test "test type casting with emulated booleans" do + test "type casting with emulated booleans" do emulate_booleans true boolean = BooleanType.create!(archived: true, published: true) @@ -55,7 +55,7 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal 0, @connection.type_cast(false) end - test "test type casting without emulated booleans" do + test "type casting without emulated booleans" do emulate_booleans false boolean = BooleanType.create!(archived: true, published: true) diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index 8826ad7fd1..e4a6ed5482 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -48,7 +48,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase test "schema dump includes collation" do output = dump_table_schema("charset_collations") - assert_match %r{t.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output - assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output + assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output + assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8d8955e5c9..a2faf43b0d 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -42,7 +42,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase @connection.update("set @@wait_timeout=1") sleep 2 assert !@connection.active? - + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end @@ -63,20 +63,33 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert @connection.active? end + def test_verify_with_args_is_deprecated + assert_deprecated do + @connection.verify!(option: true) + end + assert_deprecated do + @connection.verify!([]) + end + assert_deprecated do + @connection.verify!({}) + end + end + def test_execute_after_disconnect @connection.disconnect! + error = assert_raise(ActiveRecord::StatementInvalid) do @connection.execute("SELECT 1") end - assert_match(/closed MySQL connection/, error.message) + assert_kind_of Mysql2::Error, error.cause end def test_quote_after_disconnect @connection.disconnect! - error = assert_raise(Mysql2::Error) do + + assert_raise(Mysql2::Error) do @connection.quote("string") end - assert_match(/closed MySQL connection/, error.message) end def test_active_after_disconnect @@ -84,6 +97,22 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal false, @connection.active? end + def test_wait_timeout_as_string + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(wait_timeout: "60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + end + + def test_wait_timeout_as_url + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge("url" => "mysql2:///?wait_timeout=60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + end + def test_mysql_connection_collation_is_configured assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection") assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection") @@ -119,10 +148,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end end - def test_passing_arbitary_flags_to_adapter + def test_passing_arbitrary_flags_to_adapter run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS)) - assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] end end @@ -186,7 +215,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase "expected release_advisory_lock to return false when there was no lock to release" end - protected + private def test_lock_free(lock_name) @connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1 diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb index 135789a57d..c131a5169c 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -6,23 +6,27 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase end test "microsecond precision for MySQL gte 5.6.4" do - stub_version "5.6.4" - assert_microsecond_precision + stub_version "5.6.4" do + assert_microsecond_precision + end end test "no microsecond precision for MySQL lt 5.6.4" do - stub_version "5.6.3" - assert_no_microsecond_precision + stub_version "5.6.3" do + assert_no_microsecond_precision + end end test "microsecond precision for MariaDB gte 5.3.0" do - stub_version "5.5.5-10.1.8-MariaDB-log" - assert_microsecond_precision + stub_version "5.5.5-10.1.8-MariaDB-log" do + assert_microsecond_precision + end end test "no microsecond precision for MariaDB lt 5.3.0" do - stub_version "5.2.9-MariaDB" - assert_no_microsecond_precision + stub_version "5.2.9-MariaDB" do + assert_no_microsecond_precision + end end private @@ -41,5 +45,8 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase def stub_version(full_version_string) @connection.stubs(:full_version).returns(full_version_string) @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + yield + ensure + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 630cdb36a4..d311ffb703 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,17 +1,11 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" if ActiveRecord::Base.connection.supports_json? class Mysql2JSONTest < ActiveRecord::Mysql2TestCase - include SchemaDumpingHelper + include JSONSharedTestCases self.use_transactional_tests = false - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end - def setup @connection = ActiveRecord::Base.connection begin @@ -27,169 +21,9 @@ if ActiveRecord::Base.connection.supports_json? JsonDataType.reset_column_information end - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.change_table("json_data_type") do |t| - t.json "users" + private + def column_type + :json end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal :json, column.type - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.json\s+"settings"/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key"=>nil }, type.deserialize('{"key": null}')) - assert_equal({ "c"=>"}",'"a"'=>'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.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]}')| - x = JsonDataType.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)" - x = JsonDataType.first - assert_equal(nil, x.payload) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload:nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload:nil).first - assert_equal(nil, x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload:nil).first - assert_equal(json.reload, x) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 69336eb906..565130c38f 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -17,17 +17,6 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end end - def test_valid_column - with_example_table do - column = @conn.columns("ex").find { |col| col.name == "id" } - assert @conn.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @conn.valid_type?(:foobar) - end - def test_columns_for_distinct_zero_orders assert_equal "posts.id", @conn.columns_for_distinct("posts.id", []) @@ -65,6 +54,19 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase @conn.columns_for_distinct("posts.id", [order]) end + def test_errors_for_bigint_fks_on_integer_pk_table + # table old_cars has primary key of integer + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.add_reference :engines, :old_car + @conn.add_foreign_key :engines, :old_cars + end + + assert_match "Column `old_car_id` on table `engines` has a type of `bigint(20)`", error.message + assert_not_nil error.cause + @conn.exec_query("ALTER TABLE engines DROP COLUMN old_car_id") + end + private def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 776549eb7a..2c778b1150 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -30,11 +30,11 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() # will fail with these table names if these test cases fail - create_tables_directly "group"=>"id int auto_increment primary key, `order` varchar(255), select_id int", - "select"=>"id int auto_increment primary key", - "values"=>"id int auto_increment primary key, group_id int", - "distinct"=>"id int auto_increment primary key", - "distinct_select"=>"distinct_id int, select_id int" + create_tables_directly "group" => "id int auto_increment primary key, `order` varchar(255), select_id int", + "select" => "id int auto_increment primary key", + "values" => "id int auto_increment primary key, group_id int", + "distinct" => "id int auto_increment primary key", + "distinct_select" => "distinct_id int, select_id int" end teardown do @@ -143,7 +143,7 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase end # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly (tables, connection = @connection) + def create_tables_directly(tables, connection = @connection) tables.each do |table_name, column_properties| connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index fa54aac6b3..251a50e41e 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -16,9 +16,9 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::SchemaMigration.table_name connection.drop_table table_name, if_exists: true - connection.initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table - assert connection.column_exists?(table_name, :version, :string, collation: "utf8_general_ci") + assert connection.column_exists?(table_name, :version, :string) end end @@ -27,9 +27,9 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::InternalMetadata.table_name connection.drop_table table_name, if_exists: true - connection.initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table - assert connection.column_exists?(table_name, :key, :string, collation: "utf8_general_ci") + assert connection.column_exists?(table_name, :key, :string) end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index aea930cfe6..1fad5585de 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -76,7 +76,7 @@ module ActiveRecord table = "key_tests" indexes = @connection.indexes(table).sort_by(&:name) - assert_equal 3,indexes.size + assert_equal 3, indexes.size index_a = indexes.select { |i| i.name == index_a_name }[0] index_b = indexes.select { |i| i.name == index_b_name }[0] diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index bee42d48f1..d6e7f29a5c 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -8,7 +8,7 @@ class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase assert_equal "blob", type_to_sql(:binary) end - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) + def type_to_sql(type, limit = nil) + ActiveRecord::Base.connection.type_to_sql(type, limit: limit) end end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index edd5353ee3..16101e38cb 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -10,6 +10,8 @@ module ActiveRecord end setup do + @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + @connection = ActiveRecord::Base.connection @connection.clear_cache! @@ -25,30 +27,34 @@ module ActiveRecord teardown do @connection.drop_table "samples", if_exists: true + + Thread.abort_on_exception = @abort end test "raises Deadlocked when a deadlock is encountered" do assert_raises(ActiveRecord::Deadlocked) do + barrier = Concurrent::CyclicBarrier.new(2) + s1 = Sample.create value: 1 s2 = Sample.create value: 2 thread = Thread.new do Sample.transaction do s1.lock! - sleep 1 + barrier.wait s2.update_attributes value: 1 end end - sleep 0.5 - - Sample.transaction do - s2.lock! - sleep 1 - s1.update_attributes value: 2 + begin + Sample.transaction do + s2.lock! + barrier.wait + s1.update_attributes value: 2 + end + ensure + thread.join end - - thread.join 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 3df11ce11b..71dcfaa241 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -15,6 +15,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase t.bigint :unsigned_bigint, unsigned: true t.float :unsigned_float, unsigned: true t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 + t.column :unsigned_zerofill, "int unsigned zerofill" end end @@ -34,10 +35,10 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase assert_raise(ActiveModel::RangeError) do UnsignedType.create(unsigned_bigint: -10) end - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::RangeError) do UnsignedType.create(unsigned_float: -10.0) end - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::RangeError) do UnsignedType.create(unsigned_decimal: -10.0) end end @@ -50,16 +51,16 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 end - @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| + @connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column| assert column.unsigned? end end test "schema dump includes unsigned option" do schema = dump_table_schema "unsigned_types" - assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema - assert_match %r{t.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema - assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema - assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema + assert_match %r{t\.integer\s+"unsigned_integer",\s+unsigned: true$}, schema + assert_match %r{t\.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema + assert_match %r{t\.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t\.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb new file mode 100644 index 0000000000..442a4fb7b5 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -0,0 +1,59 @@ +require "cases/helper" +require "support/schema_dumping_helper" + +if ActiveRecord::Base.connection.supports_virtual_columns? + class Mysql2VirtualColumnTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class VirtualColumn < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :virtual_columns, force: true do |t| + t.string :name + t.virtual :upper_name, type: :string, as: "UPPER(`name`)" + t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true + end + VirtualColumn.create(name: "Rails") + end + + def teardown + @connection.drop_table :virtual_columns, if_exists: true + VirtualColumn.reset_column_information + end + + def test_virtual_column + column = VirtualColumn.columns_hash["upper_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "RAILS", VirtualColumn.take.upper_name + end + + def test_stored_column + column = VirtualColumn.columns_hash["name_length"] + assert_predicate column, :virtual? + assert_match %r{\b(?:STORED|PERSISTENT)\b}, column.extra + assert_equal 5, VirtualColumn.take.name_length + end + + def test_change_table + @connection.change_table :virtual_columns do |t| + t.virtual :lower_name, type: :string, as: "LOWER(name)" + end + VirtualColumn.reset_column_information + column = VirtualColumn.columns_hash["lower_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "rails", VirtualColumn.take.lower_name + end + + def test_schema_dumping + output = dump_table_schema("virtual_columns") + assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output) + assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output) + end + 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 d3c65f3d94..b787de8453 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -39,6 +39,10 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC)) + assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc }) + assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc }) + %w(gin gist hash btree).each do |type| expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name")) assert_equal expected, add_index(:people, :last_name, using: type) diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 97960b6c51..c78c6178ff 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -16,10 +16,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase @connection.transaction do @connection.create_table("pg_arrays") do |t| - t.string "tags", array: true + t.string "tags", array: true, limit: 255 t.integer "ratings", array: true t.datetime :datetimes, array: true t.hstore :hstores, array: true + t.decimal :decimals, array: true, default: [], precision: 10, scale: 2 + t.timestamp :timestamps, array: true, default: [], precision: 6 end end PgArray.reset_column_information @@ -34,7 +36,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_column assert_equal :string, @column.type - assert_equal "character varying", @column.sql_type + assert_equal "character varying(255)", @column.sql_type assert @column.array? assert_not @type.binary? @@ -110,22 +112,23 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_schema_dump_with_shorthand output = dump_table_schema "pg_arrays" - assert_match %r[t\.string\s+"tags",\s+array: true], output + assert_match %r[t\.string\s+"tags",\s+limit: 255,\s+array: true], output assert_match %r[t\.integer\s+"ratings",\s+array: true], output + assert_match %r[t\.decimal\s+"decimals",\s+precision: 10,\s+scale: 2,\s+default: \[\],\s+array: true], output end def test_select_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - assert_equal(["1","2","3"], x.tags) + assert_equal(["1", "2", "3"], x.tags) end def test_rewrite_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - x.tags = ["1","2","3","4"] + x.tags = ["1", "2", "3", "4"] x.save! - assert_equal ["1","2","3","4"], x.reload.tags + assert_equal ["1", "2", "3", "4"], x.reload.tags end def test_select_with_integers @@ -163,28 +166,28 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase end def test_strings_with_quotes - assert_cycle(:tags, ["this has",'some "s that need to be escaped"']) + assert_cycle(:tags, ["this has", 'some "s that need to be escaped"']) end def test_strings_with_commas - assert_cycle(:tags, ["this,has","many,values"]) + assert_cycle(:tags, ["this,has", "many,values"]) end def test_strings_with_array_delimiters - assert_cycle(:tags, ["{","}"]) + assert_cycle(:tags, ["{", "}"]) end def test_strings_with_null_strings - assert_cycle(:tags, ["NULL","NULL"]) + assert_cycle(:tags, ["NULL", "NULL"]) end def test_contains_nils - assert_cycle(:tags, ["1",nil,nil]) + assert_cycle(:tags, ["1", nil, nil]) end def test_insert_fixture tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - @connection.insert_fixture({ "tags" => tag_values }, "pg_arrays" ) + @connection.insert_fixture({ "tags" => tag_values }, "pg_arrays") assert_equal(PgArray.last.tags, tag_values) end @@ -211,7 +214,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x = PgArray.create!(tags: tags) x.reload - assert_equal x.tags_before_type_cast, PgArray.type_for_attribute("tags").serialize(tags) + refute x.changed? end def test_quoting_non_standard_delimiters @@ -219,9 +222,10 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",") semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";") + conn = PgArray.connection - assert_equal %({"hello,",world;}), comma_delim.serialize(strings) - assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) + assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings)) + assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings)) end def test_mutate_array @@ -312,9 +316,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase end def test_encoding_arrays_of_utf8_strings - string_with_utf8 = "nový" - assert_equal [string_with_utf8], @type.deserialize(@type.serialize([string_with_utf8])) - assert_equal [[string_with_utf8]], @type.deserialize(@type.serialize([[string_with_utf8]])) + arrays_of_utf8_strings = %w(nový ファイル) + assert_equal arrays_of_utf8_strings, @type.deserialize(@type.serialize(arrays_of_utf8_strings)) + assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings])) + end + + def test_precision_is_respected_on_timestamp_columns + time = Time.now.change(usec: 123) + record = PgArray.create!(timestamps: [time]) + + assert_equal 123, record.timestamps.first.usec + record.reload + assert_equal 123, record.timestamps.first.usec end private diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index dc0df8715a..539c90f0bc 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -32,9 +32,9 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_binary_columns_are_limitless_the_upper_limit_is_one_GB - assert_equal "bytea", @connection.type_to_sql(:binary, 100_000) + assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000) assert_raise ActiveRecord::ActiveRecordError do - @connection.type_to_sql :binary, 4294967295 + @connection.type_to_sql(:binary, limit: 4294967295) end end @@ -52,7 +52,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_type_case_nil - assert_equal(nil, @type.deserialize(nil)) + assert_nil(@type.deserialize(nil)) end def test_read_value @@ -66,7 +66,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_read_nil_value @connection.execute "insert into bytea_data_type (payload) VALUES (null)" record = ByteaDataType.first - assert_equal(nil, record.payload) + assert_nil(record.payload) record.delete end @@ -89,13 +89,14 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase Thread.new do other_conn = ActiveRecord::Base.connection other_conn.execute("SET standard_conforming_strings = off") + other_conn.execute("SET escape_string_warning = off") end.join test_via_to_sql end def test_write_binary - data = File.read(File.join(File.dirname(__FILE__), "..", "..", "..", "assets", "example.log")) + data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) assert(data.size > 1) record = ByteaDataType.create(payload: data) assert_not record.new_record? @@ -106,8 +107,8 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_write_nil record = ByteaDataType.create(payload: nil) assert_not record.new_record? - assert_equal(nil, record.payload) - assert_equal(nil, ByteaDataType.where(id: record.id).first.payload) + assert_nil(record.payload) + assert_nil(ByteaDataType.where(id: record.id).first.payload) end class Serializer diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb new file mode 100644 index 0000000000..03b44feab6 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase + class Default < ActiveRecord::Base; end + + def test_case_insensitiveness + connection = ActiveRecord::Base.connection + table = Default.arel_table + + column = Default.columns_hash["char1"] + comparison = connection.case_insensitive_comparison table, :char1, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["char2"] + comparison = connection.case_insensitive_comparison table, :char2, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["char3"] + comparison = connection.case_insensitive_comparison table, :char3, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["multiline_default"] + comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil + assert_match(/lower/i, comparison.to_sql) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb index b39e298a5d..a603221d8f 100644 --- a/activerecord/test/cases/adapters/postgresql/collation_test.rb +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -47,7 +47,7 @@ class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase test "schema dump includes collation" do output = dump_table_schema("postgresql_collations") - assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output - assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + assert_match %r{t\.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t\.text\s+"text_posix",\s+collation: "POSIX"$}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 48c82cb7b9..32afe331fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -31,15 +31,21 @@ module ActiveRecord end def test_encoding - assert_not_nil @connection.encoding + assert_queries(1) do + assert_not_nil @connection.encoding + end end def test_collation - assert_not_nil @connection.collation + assert_queries(1) do + assert_not_nil @connection.collation + end end def test_ctype - assert_not_nil @connection.ctype + assert_queries(1) do + assert_not_nil @connection.ctype + end end def test_default_client_min_messages @@ -90,22 +96,22 @@ module ActiveRecord end def test_tables_logs_name - ActiveSupport::Deprecation.silence { @connection.tables("hello") } + @connection.tables assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_indexes_logs_name - @connection.indexes("items", "hello") + assert_deprecated { @connection.indexes("items", "hello") } assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_table_exists_logs_name - ActiveSupport::Deprecation.silence { @connection.table_exists?("items") } + @connection.table_exists?("items") assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_table_alias_length_logs_name - @connection.instance_variable_set("@table_alias_length", nil) + @connection.instance_variable_set("@max_identifier_length", nil) @connection.table_alias_length assert_equal "SCHEMA", @subscriber.logged[0][1] end @@ -127,8 +133,8 @@ module ActiveRecord if ActiveRecord::Base.connection.prepared_statements def test_statement_key_is_logged - bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) - @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true) + binds = [bind_attribute(nil, 1)] + @connection.exec_query("SELECT $1::integer", "SQL", binds, prepare: true) name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") @@ -156,7 +162,7 @@ module ActiveRecord secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") ActiveRecord::Base.connection_pool.checkin(secondary_connection) elsif ARTest.config["with_manual_interventions"] - puts "Kill the connection now (e.g. by restarting the PostgreSQL " + + puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ 'server with the "-m fast" option) and then press enter.' $stdin.gets else @@ -175,9 +181,9 @@ module ActiveRecord new_connection_pid = @connection.query("select pg_backend_pid()") assert_not_equal original_connection_pid, new_connection_pid, - "umm -- looks like you didn't break the connection, because we're still " + + "umm -- looks like you didn't break the connection, because we're still " \ "successfully querying with the same connection pid." - + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end @@ -245,7 +251,7 @@ module ActiveRecord end end - protected + private def with_warning_suppression log_level = @connection.client_min_messages diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 0ac8b7339b..0725fde5ae 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -28,12 +28,12 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase end def test_data_type_of_time_types - assert_equal :string, @first_time.column_for_attribute(:time_interval).type - assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:scaled_time_interval).type end def test_data_type_of_oid_types - assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type + assert_equal :oid, @first_oid.column_for_attribute(:obj_id).type end def test_time_values @@ -61,9 +61,9 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase end def test_text_columns_are_limitless_the_upper_limit_is_one_GB - assert_equal "text", @connection.type_to_sql(:text, 100_000) + assert_equal "text", @connection.type_to_sql(:text, limit: 100_000) assert_raise ActiveRecord::ActiveRecordError do - @connection.type_to_sql :text, 4294967295 + @connection.type_to_sql(:text, limit: 4294967295) end end end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 7493bce4fb..d79fbccf47 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -7,14 +7,14 @@ class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase 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 + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."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 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 "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index a65d4d1ad9..c1f3a4ae2c 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -96,7 +96,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase assert_nothing_raised { PostgresqlPoint.new(x: "") } p = PostgresqlPoint.new(x: "") - assert_equal nil, p.x + assert_nil p.x end def test_array_of_points_round_trip diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 9236a67b11..f9cce10fb8 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -10,6 +10,12 @@ if ActiveRecord::Base.connection.supports_extensions? store_accessor :settings, :language, :timezone end + class FakeParameters + def to_unsafe_h + { "hi" => "hi" } + end + end + def setup @connection = ActiveRecord::Base.connection @@ -64,8 +70,8 @@ if ActiveRecord::Base.connection.supports_extensions? @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"' Hstore.reset_column_information - assert_equal({ "users"=>"read", "articles"=>"write" }, Hstore.column_defaults["permissions"]) - assert_equal({ "users"=>"read", "articles"=>"write" }, Hstore.new.permissions) + assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"]) + assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions) ensure Hstore.reset_column_information end @@ -113,8 +119,8 @@ if ActiveRecord::Base.connection.supports_extensions? def test_type_cast_hstore assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\"")) assert_equal({}, @type.deserialize("")) - assert_equal({ "key"=>nil }, @type.deserialize("key => NULL")) - assert_equal({ "c"=>"}",'"a"'=>'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) + assert_equal({ "key" => nil }, @type.deserialize("key => NULL")) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) end def test_with_store_accessors @@ -165,24 +171,43 @@ if ActiveRecord::Base.connection.supports_extensions? assert_not hstore.changed? end + def test_dirty_from_user_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + + hstore.settings = { "key" => "value", "alongkey" => "anything" } + assert_equal settings, hstore.settings + refute hstore.changed? + end + + def test_hstore_dirty_from_database_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + hstore.reload + + assert_equal settings, hstore.settings + hstore.settings = settings + refute hstore.changed? + end + def test_gen1 - assert_equal('" "=>""', @type.serialize(" "=>"")) + assert_equal('" "=>""', @type.serialize(" " => "")) end def test_gen2 - assert_equal('","=>""', @type.serialize(","=>"")) + assert_equal('","=>""', @type.serialize("," => "")) end def test_gen3 - assert_equal('"="=>""', @type.serialize("="=>"")) + assert_equal('"="=>""', @type.serialize("=" => "")) end def test_gen4 - assert_equal('">"=>""', @type.serialize(">"=>"")) + assert_equal('">"=>""', @type.serialize(">" => "")) end def test_parse1 - assert_equal({ "a"=>nil,"b"=>nil,"c"=>"NuLl","null"=>"c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 @@ -194,19 +219,19 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_parse4 - assert_equal({ "=a"=>"q=w" }, @type.deserialize('\=a=>q=w')) + assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w')) end def test_parse5 - assert_equal({ "=a"=>"q=w" }, @type.deserialize('"=a"=>q\=w')) + assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w')) end def test_parse6 - assert_equal({ "\"a"=>"q>w" }, @type.deserialize('"\"a"=>q>w')) + assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w')) end def test_parse7 - assert_equal({ "\"a"=>"q\"w" }, @type.deserialize('\"a=>q"w')) + assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w')) end def test_rewrite @@ -321,6 +346,10 @@ if ActiveRecord::Base.connection.supports_extensions? assert_match %r[t\.hstore "tags",\s+default: {}], output end + def test_supports_to_unsafe_h_values + assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new)) + end + private def assert_array_cycle(array) # test creation diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 19b00258b6..b9e177e6ec 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -30,7 +30,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase record = PostgresqlInfinity.new(float: "-Infinity") assert_equal(-Float::INFINITY, record.float) record = PostgresqlInfinity.new(float: "NaN") - assert_send [record.float, :nan?] + assert record.float.nan?, "Expected #{record.float} to be NaN" end test "update_all with infinity on a float column" do diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 273b2c1c7b..4eeb563781 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,14 +1,8 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" module PostgresqlJSONSharedTestCases - include SchemaDumpingHelper - - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end + include JSONSharedTestCases def setup @connection = ActiveRecord::Base.connection @@ -16,6 +10,7 @@ module PostgresqlJSONSharedTestCases @connection.create_table("json_data_type") do |t| t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} t.public_send column_type, "settings" # t.json 'settings' + t.public_send column_type, "objects", array: true # t.json 'objects', array: true end rescue ActiveRecord::StatementInvalid skip "do not test on PostgreSQL without #{column_type} type." @@ -27,186 +22,23 @@ module PostgresqlJSONSharedTestCases JsonDataType.reset_column_information end - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type - assert_not column.array? - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - def test_default @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } JsonDataType.reset_column_information - assert_equal({ "users"=>"read", "posts"=>["read", "write"] }, JsonDataType.column_defaults["permissions"]) - assert_equal({ "users"=>"read", "posts"=>["read", "write"] }, JsonDataType.new.permissions) - ensure - JsonDataType.reset_column_information - end - - def test_change_table_supports_json - @connection.transaction do - @connection.change_table("json_data_type") do |t| - t.public_send column_type, "users", default: "{}" # t.json 'users', default: '{}' - end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal column_type, column.type - - raise ActiveRecord::Rollback # reset the schema change - end + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.column_defaults["permissions"]) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.new.permissions) ensure JsonDataType.reset_column_information end - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key"=>nil }, type.deserialize('{"key": null}')) - assert_equal({ "c"=>"}",'"a"'=>'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.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]}')| - x = JsonDataType.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)" - x = JsonDataType.first - assert_equal(nil, x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload:nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload:nil).first - assert_equal(nil, x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload:nil).first - assert_equal(json.reload, x) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.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"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - + def test_deserialize_with_array + x = JsonDataType.new(objects: ["foo" => "bar"]) + assert_equal ["foo" => "bar"], x.objects x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload + assert_equal ["foo" => "bar"], x.objects + x.reload + assert_equal ["foo" => "bar"], x.objects end end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 834354dcc9..bfb2b7c27a 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_send [third.double, :nan?] + assert third.double.nan?, "Expected #{third.double} to be NaN" end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index e6af93a53e..bfc763e1ef 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -21,17 +21,6 @@ module ActiveRecord end end - def test_valid_column - with_example_table do - column = @connection.columns("ex").find { |col| col.name == "id" } - assert @connection.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @connection.valid_type?(:foobar) - end - def test_primary_key with_example_table do assert_equal "id", @connection.primary_key("ex") @@ -54,12 +43,6 @@ module ActiveRecord end end - def test_primary_key_raises_error_if_table_not_found - assert_raises(ActiveRecord::StatementInvalid) do - @connection.primary_key("unobtainium") - end - end - def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id", "postgresql_partitioned_table_parent_id_seq") @@ -219,8 +202,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + binds = [bind_attribute("id", 1)] + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -234,8 +217,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + binds = [bind_attribute("id", "1-fuu", Type::Integer.new)] + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -263,9 +246,12 @@ module ActiveRecord def test_index_with_opclass with_example_table do - @connection.add_index "ex", "data varchar_pattern_ops", name: "with_opclass" - index = @connection.indexes("ex").find { |idx| idx.name == "with_opclass" } + @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.remove_index "ex", "data varchar_pattern_ops" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } end end @@ -360,7 +346,7 @@ module ActiveRecord @connection.select_all "SELECT NULL::anyelement" @connection.select_all "SELECT NULL::anyelement" } - assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning) + assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'\. It will be treated as String\.\n\z/, warning) ensure reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb new file mode 100644 index 0000000000..8c62690866 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "models/computer" +require "models/developer" + +class PreparedStatementsDisabledTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers + + def setup + @conn = ActiveRecord::Base.establish_connection :arunit_without_prepared_statements + end + + def teardown + @conn.release_connection + ActiveRecord::Base.establish_connection :arunit + end + + def test_select_query_works_even_when_prepared_statements_are_disabled + assert_not Developer.connection.prepared_statements + + david = developers(:david) + + assert_equal david, Developer.where(name: "David").last # With Binds + assert_operator Developer.count, :>, 0 # Without Binds + end +end diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb deleted file mode 100644 index b898929f8a..0000000000 --- a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" -require "models/developer" - -class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase - fixtures :developers - - def setup - @default_prepared_statements = Developer.connection_config[:prepared_statements] - Developer.connection_config[:prepared_statements] = false - end - - def teardown - Developer.connection_config[:prepared_statements] = @default_prepared_statements - end - - def nothing_raised_with_falsy_prepared_statements - assert_nothing_raised do - Developer.where(id: 1) - end - end -end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 865a3a5098..a1e966b915 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require "ipaddr" module ActiveRecord module ConnectionAdapters @@ -18,12 +17,12 @@ module ActiveRecord end def test_quote_float_nan - nan = 0.0/0 + nan = 0.0 / 0 assert_equal "'NaN'", @conn.quote(nan) end def test_quote_float_infinity - infinity = 1.0/0 + infinity = 1.0 / 0 assert_equal "'Infinity'", @conn.quote(infinity) end @@ -36,7 +35,7 @@ module ActiveRecord def test_quote_bit_string value = "'); SELECT * FROM users; /*\n01\n*/--" type = OID::Bit.new - assert_equal nil, @conn.quote(type.serialize(value)) + assert_nil @conn.quote(type.serialize(value)) end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 7193f23880..f86a76e08a 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -68,24 +68,13 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase USERS.each do |u| @connection.clear_cache! set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_param(1)]) + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_attribute("id", 1)]) set_session_auth end end end end - def test_schema_uniqueness - assert_nothing_raised do - set_session_auth - USERS.each do |u| - set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") - set_session_auth - end - end - end - def test_sequence_schema_caching assert_nothing_raised do USERS.each do |u| @@ -110,10 +99,6 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase private def set_session_auth(auth = nil) - @connection.session_auth = auth || "default" - end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + @connection.session_auth = auth || "default" end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 51a2306c59..f6b957476b 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -91,6 +91,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)" + @connection.execute "CREATE TABLE #{SCHEMA2_NAME}.#{PK_TABLE_NAME} (id serial primary key)" @connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))" end @@ -168,17 +169,17 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do - @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = ?", "sql", [bind_attribute("id", 1)] end end if ActiveRecord::Base.connection.prepared_statements def test_schema_change_with_prepared_stmt altered = false - @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] @connection.exec_query "alter table developers add column zomg int", "sql", [] altered = true - @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] ensure # We are not using DROP COLUMN IF EXISTS because that syntax is only # supported by pg 9.X @@ -301,13 +302,13 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_index_name_exists with_schema_search_path(SCHEMA_NAME) do - assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) - assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index", true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index") end end @@ -361,19 +362,11 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end def test_primary_key_assuming_schema_search_path - with_schema_search_path(SCHEMA_NAME) do + with_schema_search_path("#{SCHEMA_NAME}, #{SCHEMA2_NAME}") do assert_equal "id", @connection.primary_key(PK_TABLE_NAME), "primary key should be found" end end - def test_primary_key_raises_error_if_table_not_found_on_schema_search_path - with_schema_search_path(SCHEMA2_NAME) do - assert_raises(ActiveRecord::StatementInvalid) do - @connection.primary_key(PK_TABLE_NAME) - end - end - end - def test_pk_and_sequence_for_with_schema_specified pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name [ @@ -383,7 +376,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase pk, seq = @connection.pk_and_sequence_for(given) assert_equal "id", pk, "primary key should be found when table referenced as #{given}" assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") - assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") end end @@ -393,7 +386,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase SCHEMA_NAME => SCHEMA_NAME, %(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME, %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => "public" - }.each do |given,expect| + }.each do |given, expect| with_schema_search_path(given) { assert_equal expect, @connection.current_schema } end end @@ -418,7 +411,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase SCHEMA_NAME => true, SCHEMA2_NAME => true, "darkside" => false - }.each do |given,expect| + }.each do |given, expect| assert_equal expect, @connection.schema_exists?(given) end end @@ -474,10 +467,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal this_index_column, this_index.columns[0] assert_equal this_index_name, this_index.name end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) - end end class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index eb9978a898..146b619a4b 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -3,13 +3,13 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter - class InactivePGconn + class InactivePgConnection def query(*args) - raise PGError + raise PG::Error end def status - PGconn::CONNECTION_BAD + PG::CONNECTION_BAD end end @@ -31,7 +31,7 @@ module ActiveRecord end def test_dealloc_does_not_raise_on_inactive_connection - cache = StatementPool.new InactivePGconn.new, 10 + cache = StatementPool.new InactivePgConnection.new, 10 cache["foo"] = "bar" assert_nothing_raised { cache.clear } end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index e7c1d97d16..962450aada 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -21,7 +21,7 @@ class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase @connection.reconnect! timestamp = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time assert_instance_of Time, timestamp.time end ensure @@ -35,7 +35,7 @@ class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase @connection.execute("SET time zone 'America/Jamaica'", "SCHEMA") timestamp = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time assert_instance_of Time, timestamp.time end ensure diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index d992e22305..9b42d0383d 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require "support/connection_helper" +require "concurrent/atomic/cyclic_barrier" module ActiveRecord class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase @@ -10,6 +11,8 @@ module ActiveRecord end setup do + @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + @connection = ActiveRecord::Base.connection @connection.transaction do @@ -24,35 +27,34 @@ module ActiveRecord teardown do @connection.drop_table "samples", if_exists: true + + Thread.abort_on_exception = @abort end test "raises SerializationFailure when a serialization failure occurs" do - with_warning_suppression do - assert_raises(ActiveRecord::SerializationFailure) do - thread = Thread.new do - Sample.transaction isolation: :serializable do - Sample.delete_all + assert_raises(ActiveRecord::SerializationFailure) do + before = Concurrent::CyclicBarrier.new(2) + after = Concurrent::CyclicBarrier.new(2) - 10.times do |i| - sleep 0.1 - - Sample.create value: i - end + thread = Thread.new do + with_warning_suppression do + Sample.transaction isolation: :serializable do + before.wait + Sample.create value: Sample.sum(:value) + after.wait end end + end - sleep 0.1 - - Sample.transaction isolation: :serializable do - Sample.delete_all - - 10.times do |i| - sleep 0.1 - - Sample.create value: i + begin + with_warning_suppression do + Sample.transaction isolation: :serializable do + before.wait + Sample.create value: Sample.sum(:value) + after.wait end end - + ensure thread.join end end @@ -61,37 +63,40 @@ module ActiveRecord test "raises Deadlocked when a deadlock is encountered" do with_warning_suppression do assert_raises(ActiveRecord::Deadlocked) do + barrier = Concurrent::CyclicBarrier.new(2) + s1 = Sample.create value: 1 s2 = Sample.create value: 2 thread = Thread.new do Sample.transaction do s1.lock! - sleep 1 + barrier.wait s2.update_attributes value: 1 end end - sleep 0.5 - - Sample.transaction do - s2.lock! - sleep 1 - s1.update_attributes value: 2 + begin + Sample.transaction do + s2.lock! + barrier.wait + s1.update_attributes value: 2 + end + ensure + thread.join end - - thread.join end end end - protected + private def with_warning_suppression - log_level = @connection.client_min_messages - @connection.client_min_messages = "error" + log_level = ActiveRecord::Base.connection.client_min_messages + ActiveRecord::Base.connection.client_min_messages = "error" yield - @connection.client_min_messages = log_level + ensure + ActiveRecord::Base.connection.client_min_messages = log_level end end end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index bd45a9daa0..784d77a8d1 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -19,7 +19,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase big_array = [123456789123456789] assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } - assert_equal "{123456789123456789}", bigint_array.serialize(big_array) + assert_equal "{123456789123456789}", @connection.type_cast(bigint_array.serialize(big_array)) end test "range types correctly respect registration of subtypes" do diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 01c597beae..9f9e3bda2f 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -7,13 +7,13 @@ class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase def test_extract_schema_qualified_name { - %(table_name) => [nil,"table_name"], - %("table.name") => [nil,"table.name"], + %(table_name) => [nil, "table_name"], + %("table.name") => [nil, "table.name"], %(schema.table_name) => %w{schema table_name}, %("schema".table_name) => %w{schema table_name}, %(schema."table_name") => %w{schema table_name}, %("schema"."table_name") => %w{schema table_name}, - %("even spaces".table) => ["even spaces","table"], + %("even spaces".table) => ["even spaces", "table"], %(schema."table.name") => ["schema", "table.name"] }.each do |given, expect| assert_equal Name.new(*expect), extract_schema_qualified_name(given) @@ -56,7 +56,7 @@ class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase test "can be used as hash key" do hash = { Name.new("schema", "article_seq") => "success" } assert_equal "success", hash[Name.new("schema", "article_seq")] - assert_equal nil, hash[Name.new("schema", "articles")] - assert_equal nil, hash[Name.new("public", "article_seq")] + assert_nil hash[Name.new("schema", "articles")] + assert_nil hash[Name.new("public", "article_seq")] end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 9a59691737..6ebe9d82a7 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -9,6 +9,14 @@ module PostgresqlUUIDHelper def drop_table(name) connection.drop_table name, if_exists: true end + + def uuid_function + connection.supports_pgcrypto_uuid? ? "gen_random_uuid()" : "uuid_generate_v4()" + end + + def uuid_default + connection.supports_pgcrypto_uuid? ? {} : { default: uuid_function } + end end class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase @@ -21,6 +29,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase setup do enable_extension!("uuid-ossp", connection) + enable_extension!("pgcrypto", connection) if connection.supports_pgcrypto_uuid? connection.create_table "uuid_data_type" do |t| t.uuid "guid" @@ -31,14 +40,22 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase drop_table "uuid_data_type" end + if ActiveRecord::Base.connection.supports_pgcrypto_uuid? + def test_uuid_column_default + connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()" + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert_equal "gen_random_uuid()", column.default_function + end + end + def test_change_column_default - @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" + connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] assert_equal "uuid_generate_v1()", column.default_function - @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" - + connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] assert_equal "uuid_generate_v4()", column.default_function @@ -46,6 +63,16 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information end + def test_add_column_with_null_true_and_default_nil + assert_nothing_raised do + connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil + end + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert column.null + assert_nil column.default + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type @@ -58,12 +85,12 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase def test_treat_blank_uuid_as_nil UUIDType.create! guid: "" - assert_equal(nil, UUIDType.last.guid) + assert_nil(UUIDType.last.guid) end def test_treat_invalid_uuid_as_nil uuid = UUIDType.create! guid: "foobar" - assert_equal(nil, uuid.guid) + assert_nil(uuid.guid) end def test_invalid_uuid_dont_modify_before_type_cast @@ -155,7 +182,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase # to test dumping tables which columns have defaults with custom functions connection.execute <<-SQL CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid - AS $$ SELECT * FROM uuid_generate_v4() $$ + AS $$ SELECT * FROM #{uuid_function} $$ LANGUAGE SQL VOLATILE; SQL @@ -164,11 +191,16 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase t.string "name" t.uuid "other_uuid_2", default: "my_uuid_generator()" end + + connection.create_table("pg_uuids_3", id: :uuid, **uuid_default) do |t| + t.string "name" + end end teardown do drop_table "pg_uuids" drop_table "pg_uuids_2" + drop_table "pg_uuids_3" connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();" end @@ -192,7 +224,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase def test_pk_and_sequence_for_uuid_primary_key pk, seq = connection.pk_and_sequence_for("pg_uuids") assert_equal "id", pk - assert_equal nil, seq + assert_nil seq end def test_schema_dumper_for_uuid_primary_key @@ -206,6 +238,34 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) end + + def test_schema_dumper_for_uuid_primary_key_default + schema = dump_table_schema "pg_uuids_3" + if connection.supports_pgcrypto_uuid? + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema) + else + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + end + end + + def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end end end @@ -237,6 +297,25 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase schema = dump_table_schema "pg_uuids" assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) end + + def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid, default: nil) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end end end @@ -255,10 +334,10 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase setup do connection.transaction do - connection.create_table("pg_uuid_posts", id: :uuid) do |t| + connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t| t.string "title" end - connection.create_table("pg_uuid_comments", id: :uuid) do |t| + connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t| t.references :uuid_post, type: :uuid t.string "content" end @@ -282,7 +361,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase assert_raise ActiveRecord::RecordNotFound do UuidPost.find(123456) end - end def test_find_by_with_uuid diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb index 28e8f12c18..dd88ed3656 100644 --- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -47,7 +47,7 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase test "schema dump includes collation" do output = dump_table_schema("collation_table_sqlite3") - assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output - assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output + assert_match %r{t\.string\s+"string_nocase",\s+collation: "NOCASE"$}, output + assert_match %r{t\.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output end end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 8342b05870..e1cfd703e8 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -41,8 +41,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase test_copy_table("comments", "comments_with_index") do @connection.add_index("comments_with_index", ["post_id", "type"]) test_copy_table("comments_with_index", "comments_with_index2") do - assert_equal table_indexes_without_name("comments_with_index"), - table_indexes_without_name("comments_with_index2") + assert_nil table_indexes_without_name("comments_with_index") + assert_nil table_indexes_without_name("comments_with_index2") end end end @@ -59,7 +59,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase copied_id = @connection.columns("goofy_string_id2").detect { |col| col.name == "id" } assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type - assert_equal original_id.limit, copied_id.limit + assert_nil original_id.limit + assert_nil copied_id.limit end end @@ -75,7 +76,7 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase test_copy_table "binaries", "binaries2" end -protected +private def copy_table(from, to, options = {}) @connection.copy_table(from, to, { temporary: true }.merge(options)) end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 128acb79cf..29d97ae78c 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -7,13 +7,13 @@ class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase 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 %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers 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 %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 %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 80a37e83ff..aefbb309e6 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -1,6 +1,5 @@ require "cases/helper" require "bigdecimal" -require "yaml" require "securerandom" class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase @@ -15,31 +14,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal expected, @conn.type_cast(binary) end - def test_type_cast_symbol - assert_equal "foo", @conn.type_cast(:foo) - end - - def test_type_cast_date - date = Date.today - expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date) - end - - def test_type_cast_time - time = Time.now - expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time) - end - - def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10) - assert_equal 2.2, @conn.type_cast(2.2) - end - - def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil) - end - def test_type_cast_true assert_equal "t", @conn.type_cast(true) end @@ -53,31 +27,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal bd.to_f, @conn.type_cast(bd) end - def test_type_cast_unknown_should_raise_error - obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj) } - end - - def test_type_cast_object_which_responds_to_quoted_id - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - - def id - 10 - end - }.new - assert_equal 10, @conn.type_cast(quoted_id_obj) - - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } - end - def test_quoting_binary_strings value = "hello".encode("ascii-8bit") type = ActiveRecord::Type::String.new diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 66f9349111..9a812e325e 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -49,22 +49,6 @@ module ActiveRecord end end - def test_valid_column - with_example_table do - column = @conn.columns("ex").find { |col| col.name == "id" } - assert @conn.valid_type?(column.type) - end - end - - # sqlite3 databases should be able to support any type and not just the - # ones mentioned in the native_database_types. - # - # Therefore test_invalid column should always return true even if the - # type is not valid. - def test_invalid_column - assert @conn.valid_type?(:foobar) - end - def test_column_types owner = Owner.create!(name: "hello".encode("ascii-8bit")) owner.reload @@ -82,11 +66,11 @@ module ActiveRecord def test_exec_insert with_example_table do - vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)] - @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", vals) + binds = [bind_attribute("number", 10)] + @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", binds) result = @conn.exec_query( - "select number from ex where number = ?", "SQL", vals) + "select number from ex where number = ?", "SQL", binds) assert_equal 1, result.rows.length assert_equal 10, result.rows.first.first @@ -150,7 +134,7 @@ module ActiveRecord with_example_table "id int, data string" do @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", 1)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -164,7 +148,7 @@ module ActiveRecord @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", "1-fuu", Type::Integer.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -190,7 +174,7 @@ module ActiveRecord end def test_type_cast_should_not_mutate_encoding - name = "hello".force_encoding(Encoding::ASCII_8BIT) + name = "hello".force_encoding(Encoding::ASCII_8BIT) Owner.create(name: name) assert_equal Encoding::ASCII_8BIT, name.encoding ensure @@ -267,29 +251,26 @@ module ActiveRecord def test_tables with_example_table do - ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } + assert_equal %w{ ex }, @conn.tables with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer", "people" do - ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } + assert_equal %w{ ex people }.sort, @conn.tables.sort end end end def test_tables_logs_name sql = <<-SQL - SELECT name FROM sqlite_master - WHERE type IN ('table','view') AND name <> 'sqlite_sequence' + SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do - ActiveSupport::Deprecation.silence do - @conn.tables("hello") - end + @conn.tables end end def test_indexes_logs_name with_example_table do assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do - @conn.indexes("ex", "hello") + assert_deprecated { @conn.indexes("ex", "hello") } end end end @@ -297,13 +278,10 @@ module ActiveRecord def test_table_exists_logs_name with_example_table do sql = <<-SQL - SELECT name FROM sqlite_master - WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' + SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do - ActiveSupport::Deprecation.silence do - assert @conn.table_exists?("ex") - end + assert @conn.table_exists?("ex") end end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index aebcce3691..37ff973397 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -3,7 +3,6 @@ require "cases/helper" class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = ActiveRecord::ConnectionAdapters::SQLite3Adapter::StatementPool.new(10) cache["foo"] = "bar" assert_equal "bar", cache["foo"] diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index e3eccad71f..5b608d8e83 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -1,146 +1,143 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_migrations? +class ActiveRecordSchemaTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + setup do + @original_verbose = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + @connection = ActiveRecord::Base.connection + ActiveRecord::SchemaMigration.drop_table + end - class ActiveRecordSchemaTest < ActiveRecord::TestCase - self.use_transactional_tests = false + teardown do + @connection.drop_table :fruits rescue nil + @connection.drop_table :nep_fruits rescue nil + @connection.drop_table :nep_schema_migrations rescue nil + @connection.drop_table :has_timestamps rescue nil + @connection.drop_table :multiple_indexes rescue nil + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = @original_verbose + end - setup do - @original_verbose = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - @connection = ActiveRecord::Base.connection - ActiveRecord::SchemaMigration.drop_table - end + def test_has_primary_key + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + assert_equal "version", ActiveRecord::SchemaMigration.primary_key - teardown do - @connection.drop_table :fruits rescue nil - @connection.drop_table :nep_fruits rescue nil - @connection.drop_table :nep_schema_migrations rescue nil - @connection.drop_table :has_timestamps rescue nil - @connection.drop_table :multiple_indexes rescue nil - ActiveRecord::SchemaMigration.delete_all rescue nil - ActiveRecord::Migration.verbose = @original_verbose + ActiveRecord::SchemaMigration.create_table + assert_difference "ActiveRecord::SchemaMigration.count", 1 do + ActiveRecord::SchemaMigration.create version: 12 end + ensure + ActiveRecord::SchemaMigration.drop_table + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type + end - def test_has_primary_key - old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type - ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_equal "version", ActiveRecord::SchemaMigration.primary_key - - ActiveRecord::SchemaMigration.create_table - assert_difference "ActiveRecord::SchemaMigration.count", 1 do - ActiveRecord::SchemaMigration.create version: 12 + def test_schema_define + ActiveRecord::Schema.define(version: 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string end - ensure - ActiveRecord::SchemaMigration.drop_table - ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end - def test_schema_define - ActiveRecord::Schema.define(version: 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end - end - - 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 - end + 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 + end - def test_schema_define_w_table_name_prefix - table_name = ActiveRecord::SchemaMigration.table_name - old_table_name_prefix = ActiveRecord::Base.table_name_prefix - ActiveRecord::Base.table_name_prefix = "nep_" - ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" - ActiveRecord::Schema.define(version: 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end + def test_schema_define_w_table_name_prefix + table_name = ActiveRecord::SchemaMigration.table_name + old_table_name_prefix = ActiveRecord::Base.table_name_prefix + ActiveRecord::Base.table_name_prefix = "nep_" + ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" + ActiveRecord::Schema.define(version: 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string end - assert_equal 7, ActiveRecord::Migrator::current_version - ensure - ActiveRecord::Base.table_name_prefix = old_table_name_prefix - ActiveRecord::SchemaMigration.table_name = table_name end + assert_equal 7, ActiveRecord::Migrator::current_version + ensure + ActiveRecord::Base.table_name_prefix = old_table_name_prefix + ActiveRecord::SchemaMigration.table_name = table_name + end - def test_schema_raises_an_error_for_invalid_column_type - assert_raise NoMethodError do - ActiveRecord::Schema.define(version: 8) do - create_table :vegetables do |t| - t.unknown :color - end + def test_schema_raises_an_error_for_invalid_column_type + assert_raise NoMethodError do + ActiveRecord::Schema.define(version: 8) do + create_table :vegetables do |t| + t.unknown :color end end end + end - def test_schema_subclass - Class.new(ActiveRecord::Schema).define(version: 9) do - create_table :fruits - end - assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + def test_schema_subclass + Class.new(ActiveRecord::Schema).define(version: 9) do + create_table :fruits end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + end - def test_normalize_version - assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") - assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") - assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") - assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") - end + def test_normalize_version + assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") + assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") + assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") + assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + end - def test_schema_load_with_multiple_indexes_for_column_of_different_names - ActiveRecord::Schema.define do - create_table :multiple_indexes do |t| - t.string "foo" - t.index ["foo"], name: "multiple_indexes_foo_1" - t.index ["foo"], name: "multiple_indexes_foo_2" - end + def test_schema_load_with_multiple_indexes_for_column_of_different_names + ActiveRecord::Schema.define do + create_table :multiple_indexes do |t| + t.string "foo" + t.index ["foo"], name: "multiple_indexes_foo_1" + t.index ["foo"], name: "multiple_indexes_foo_2" end + end - indexes = @connection.indexes("multiple_indexes") + indexes = @connection.indexes("multiple_indexes") - assert_equal 2, indexes.length - assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort - end + assert_equal 2, indexes.length + assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort + end - def test_timestamps_without_null_set_null_to_false_on_create_table - ActiveRecord::Schema.define do - create_table :has_timestamps do |t| - t.timestamps - end + def test_timestamps_without_null_set_null_to_false_on_create_table + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps end - - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end - def test_timestamps_without_null_set_null_to_false_on_change_table - ActiveRecord::Schema.define do - create_table :has_timestamps + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + end - change_table :has_timestamps do |t| - t.timestamps default: Time.now - end - end + def test_timestamps_without_null_set_null_to_false_on_change_table + ActiveRecord::Schema.define do + create_table :has_timestamps - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + change_table :has_timestamps do |t| + t.timestamps default: Time.now + end end - def test_timestamps_without_null_set_null_to_false_on_add_timestamps - ActiveRecord::Schema.define do - create_table :has_timestamps - add_timestamps :has_timestamps, default: Time.now - end + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + def test_timestamps_without_null_set_null_to_false_on_add_timestamps + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps, default: Time.now end + + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 2418346d1b..c8b26232b6 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -116,6 +116,44 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase ActiveRecord::Base.belongs_to_required_by_default = original_value end + def test_default + david = developers(:david) + jamis = developers(:jamis) + + model = Class.new(ActiveRecord::Base) do + self.table_name = "ships" + def self.name; "Temp"; end + belongs_to :developer, default: -> { david } + end + + ship = model.create! + assert_equal david, ship.developer + + ship = model.create!(developer: jamis) + assert_equal jamis, ship.developer + + ship.update!(developer: nil) + assert_equal david, ship.developer + end + + def test_default_with_lambda + model = Class.new(ActiveRecord::Base) do + self.table_name = "ships" + def self.name; "Temp"; end + belongs_to :developer, default: -> { default_developer } + + def default_developer + Developer.first + end + end + + ship = model.create! + assert_equal developers(:david), ship.developer + + ship = model.create!(developer: developers(:jamis)) + assert_equal developers(:jamis), ship.developer + end + def test_default_scope_on_relations_is_not_cached counter = 0 @@ -285,12 +323,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_failing_create! - client = Client.create!(name: "Jimmy") + client = Client.create!(name: "Jimmy") assert_raise(ActiveRecord::RecordInvalid) { client.create_account! } assert_not_nil client.account assert client.account.new_record? end + def test_reloading_the_belonging_object + odegy_account = accounts(:odegy_account) + + assert_equal "Odegy", odegy_account.firm.name + Company.where(id: odegy_account.firm_id).update_all(name: "ODEGY") + assert_equal "Odegy", odegy_account.firm.name + + assert_equal "ODEGY", odegy_account.reload_firm.name + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil @@ -346,7 +394,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_with_select assert_equal 1, Company.find(2).firm_with_select.attributes.size - assert_equal 1, Company.all.merge!(includes: :firm_with_select ).find(2).firm_with_select.attributes.size + assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size end def test_belongs_to_without_counter_cache_option @@ -1047,7 +1095,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase comment.parent = nil comment.save! - assert_equal nil, comment.reload.parent + assert_nil comment.reload.parent assert_equal 0, comments(:greetings).reload.children_count end @@ -1062,6 +1110,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, parent.reload.children_count end + def test_belongs_to_with_out_of_range_value_assigning + model = Class.new(Comment) do + def self.name; "Temp"; end + validates :post, presence: true + end + + comment = model.new + comment.post_id = 9223372036854775808 # out of range in the bigint + + assert_nil comment.post + assert_not comment.valid? + assert_equal [{ error: :blank }], comment.errors.details[:post] + end + def test_polymorphic_with_custom_primary_key toy = Toy.create! sponsor = Sponsor.create!(sponsorable: toy) @@ -1107,12 +1169,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Column.create! record: record assert_equal 1, Column.count end - - def test_association_force_reload_with_only_true_is_deprecated - client = Client.find(3) - - assert_deprecated { client.firm(true) } - end end class BelongsToWithForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 2f62d0367e..f9d1e44595 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -7,7 +7,7 @@ require "models/computer" require "models/company" class AssociationCallbacksTest < ActiveRecord::TestCase - fixtures :posts, :authors, :projects, :developers + fixtures :posts, :authors, :author_addresses, :projects, :developers def setup @david = authors(:david) @@ -109,7 +109,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase def self.name; Project.name; end has_and_belongs_to_many :developers_with_callbacks, class_name: "Developer", - before_add: lambda { |o,r| + before_add: lambda { |o, r| dev = r new_dev = r.new_record? } @@ -128,7 +128,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert ar.developers_log.empty? alice = Developer.new(name: "alice") ar.developers_with_callbacks << alice - assert_equal"after_adding#{alice.id}", ar.developers_log.last + assert_equal "after_adding#{alice.id}", ar.developers_log.last bob = ar.developers_with_callbacks.create(name: "bob") assert_equal "after_adding#{bob.id}", ar.developers_log.last @@ -159,7 +159,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase activerecord.reload assert activerecord.developers_with_callbacks.size == 2 end - activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}","after_removing#{d.id}"] }.sort + 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? end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index e87431bf32..3638c87968 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -12,7 +12,7 @@ require "models/vertex" require "models/edge" class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, + fixtures :authors, :author_addresses, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels @@ -20,7 +20,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum,i| sum+i } + assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i } end def test_eager_association_loading_with_cascaded_two_levels_and_one_level @@ -28,7 +28,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum,i| sum+i } + assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i } assert_equal 1, authors[0].categorizations.size assert_equal 2, authors[1].categorizations.size end @@ -86,7 +86,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum,i| sum+i } + assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i } end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference @@ -183,6 +183,6 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 1, authors[1].comments.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i } + assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum + i } end end diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index aa82b9dd2a..4f0fe3236e 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -15,8 +15,8 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase end def generate_test_objects - post = Namespaced::Post.create( title: "Great stuff", body: "This is not", author_id: 1 ) - Tagging.create( taggable: post ) + post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) + Tagging.create(taggable: post) end def test_class_names diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index a7a8c6a783..e9f551b6b2 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -12,7 +12,7 @@ module Remembered included do after_create :remember - protected + private def remember; self.class.remembered << self; end end @@ -39,7 +39,7 @@ class Triangle < ActiveRecord::Base has_many :shape_expressions, as: :shape include Remembered end -class PaintColor < ActiveRecord::Base +class PaintColor < ActiveRecord::Base has_many :shape_expressions, as: :paint belongs_to :non_poly, foreign_key: "non_poly_one_id", class_name: "NonPolyOne" include Remembered diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 5d1c1c4b9b..16eff15026 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,147 +1,146 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_migrations? - class EagerSingularizationTest < ActiveRecord::TestCase - class Virus < ActiveRecord::Base - belongs_to :octopus - end - - class Octopus < ActiveRecord::Base - has_one :virus - end - - class Pass < ActiveRecord::Base - belongs_to :bus - end - - class Bus < ActiveRecord::Base - has_many :passes - end - - class Mess < ActiveRecord::Base - has_and_belongs_to_many :crises - end - - class Crisis < ActiveRecord::Base - has_and_belongs_to_many :messes - has_many :analyses, dependent: :destroy - has_many :successes, through: :analyses - has_many :dresses, dependent: :destroy - has_many :compresses, through: :dresses - end - - class Analysis < ActiveRecord::Base - belongs_to :crisis - belongs_to :success - end - - class Success < ActiveRecord::Base - has_many :analyses, dependent: :destroy - has_many :crises, through: :analyses - end - - class Dress < ActiveRecord::Base - belongs_to :crisis - has_many :compresses - end - - class Compress < ActiveRecord::Base - belongs_to :dress - end - - def setup - connection.create_table :viri do |t| - t.column :octopus_id, :integer - t.column :species, :string - end - connection.create_table :octopi do |t| - t.column :species, :string - end - connection.create_table :passes do |t| - t.column :bus_id, :integer - t.column :rides, :integer - end - connection.create_table :buses do |t| - t.column :name, :string - end - connection.create_table :crises_messes, id: false do |t| - t.column :crisis_id, :integer - t.column :mess_id, :integer - end - connection.create_table :messes do |t| - t.column :name, :string - end - connection.create_table :crises do |t| - t.column :name, :string - end - connection.create_table :successes do |t| - t.column :name, :string - end - connection.create_table :analyses do |t| - t.column :crisis_id, :integer - t.column :success_id, :integer - end - connection.create_table :dresses do |t| - t.column :crisis_id, :integer - end - connection.create_table :compresses do |t| - t.column :dress_id, :integer - end - end - - teardown do - connection.drop_table :viri - connection.drop_table :octopi - connection.drop_table :passes - connection.drop_table :buses - connection.drop_table :crises_messes - connection.drop_table :messes - connection.drop_table :crises - connection.drop_table :successes - connection.drop_table :analyses - connection.drop_table :dresses - connection.drop_table :compresses - end +class EagerSingularizationTest < ActiveRecord::TestCase + class Virus < ActiveRecord::Base + belongs_to :octopus + end - def connection - ActiveRecord::Base.connection + class Octopus < ActiveRecord::Base + has_one :virus + end + + class Pass < ActiveRecord::Base + belongs_to :bus + end + + class Bus < ActiveRecord::Base + has_many :passes + end + + class Mess < ActiveRecord::Base + has_and_belongs_to_many :crises + end + + class Crisis < ActiveRecord::Base + has_and_belongs_to_many :messes + has_many :analyses, dependent: :destroy + has_many :successes, through: :analyses + has_many :dresses, dependent: :destroy + has_many :compresses, through: :dresses + end + + class Analysis < ActiveRecord::Base + belongs_to :crisis + belongs_to :success + end + + class Success < ActiveRecord::Base + has_many :analyses, dependent: :destroy + has_many :crises, through: :analyses + end + + class Dress < ActiveRecord::Base + belongs_to :crisis + has_many :compresses + end + + class Compress < ActiveRecord::Base + belongs_to :dress + end + + def setup + connection.create_table :viri do |t| + t.column :octopus_id, :integer + t.column :species, :string end + connection.create_table :octopi do |t| + t.column :species, :string + end + connection.create_table :passes do |t| + t.column :bus_id, :integer + t.column :rides, :integer + end + connection.create_table :buses do |t| + t.column :name, :string + end + connection.create_table :crises_messes, id: false do |t| + t.column :crisis_id, :integer + t.column :mess_id, :integer + end + connection.create_table :messes do |t| + t.column :name, :string + end + connection.create_table :crises do |t| + t.column :name, :string + end + connection.create_table :successes do |t| + t.column :name, :string + end + connection.create_table :analyses do |t| + t.column :crisis_id, :integer + t.column :success_id, :integer + end + connection.create_table :dresses do |t| + t.column :crisis_id, :integer + end + connection.create_table :compresses do |t| + t.column :dress_id, :integer + end + end - def test_eager_no_extra_singularization_belongs_to - assert_nothing_raised do - Virus.all.merge!(includes: :octopus).to_a - end + teardown do + connection.drop_table :viri + connection.drop_table :octopi + connection.drop_table :passes + connection.drop_table :buses + connection.drop_table :crises_messes + connection.drop_table :messes + connection.drop_table :crises + connection.drop_table :successes + connection.drop_table :analyses + connection.drop_table :dresses + connection.drop_table :compresses + end + + def test_eager_no_extra_singularization_belongs_to + assert_nothing_raised do + Virus.all.merge!(includes: :octopus).to_a end + end - def test_eager_no_extra_singularization_has_one - assert_nothing_raised do - Octopus.all.merge!(includes: :virus).to_a - end + def test_eager_no_extra_singularization_has_one + assert_nothing_raised do + Octopus.all.merge!(includes: :virus).to_a end + end - def test_eager_no_extra_singularization_has_many - assert_nothing_raised do - Bus.all.merge!(includes: :passes).to_a - end + def test_eager_no_extra_singularization_has_many + assert_nothing_raised do + Bus.all.merge!(includes: :passes).to_a end + end - def test_eager_no_extra_singularization_has_and_belongs_to_many - assert_nothing_raised do - Crisis.all.merge!(includes: :messes).to_a - Mess.all.merge!(includes: :crises).to_a - end + def test_eager_no_extra_singularization_has_and_belongs_to_many + assert_nothing_raised do + Crisis.all.merge!(includes: :messes).to_a + Mess.all.merge!(includes: :crises).to_a end + end - def test_eager_no_extra_singularization_has_many_through_belongs_to - assert_nothing_raised do - Crisis.all.merge!(includes: :successes).to_a - end + def test_eager_no_extra_singularization_has_many_through_belongs_to + assert_nothing_raised do + Crisis.all.merge!(includes: :successes).to_a end + end - def test_eager_no_extra_singularization_has_many_through_has_many - assert_nothing_raised do - Crisis.all.merge!(includes: :compresses).to_a - end + def test_eager_no_extra_singularization_has_many_through_has_many + assert_nothing_raised do + Crisis.all.merge!(includes: :compresses).to_a end end + + private + def connection + ActiveRecord::Base.connection + end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index d1c4c1cef8..4271a09c9b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -241,7 +241,7 @@ class EagerAssociationTest < ActiveRecord::TestCase post = assert_queries(1) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) } # find the post, then find the author which is null so no query for the author or address assert_no_queries do - assert_equal nil, post.author_with_address + assert_nil post.author_with_address end end @@ -250,7 +250,7 @@ class EagerAssociationTest < ActiveRecord::TestCase sponsor.update!(sponsorable: nil) sponsor = assert_queries(1) { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) } assert_no_queries do - assert_equal nil, sponsor.sponsorable + assert_nil sponsor.sponsorable end end @@ -261,7 +261,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) } end assert_no_queries do - assert_equal nil, sponsor.sponsorable + assert_nil sponsor.sponsorable end end @@ -356,31 +356,31 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_limit comments = Comment.all.merge!(includes: :post, limit: 5, order: "comments.id").to_a assert_equal 5, comments.length - assert_equal [1,2,3,5,6], comments.collect(&:id) + assert_equal [1, 2, 3, 5, 6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect(&:id) + assert_equal [5, 6, 7], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset comments = Comment.all.merge!(includes: :post, limit: 3, offset: 2, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [3,5,6], comments.collect(&:id) + assert_equal [3, 5, 6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, offset: 1, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect(&:id) + assert_equal [6, 7, 8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array - comments = Comment.all.merge!(includes: :post, where: ["post_id = ?",4], limit: 3, offset: 1, order: "comments.id").to_a + comments = Comment.all.merge!(includes: :post, where: ["post_id = ?", 4], limit: 3, offset: 1, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect(&:id) + assert_equal [6, 7, 8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name @@ -395,14 +395,14 @@ class EagerAssociationTest < ActiveRecord::TestCase comments = Comment.all.merge!(includes: :post, where: { posts: { id: 4 } }, limit: 3, order: "comments.id").to_a end assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect(&:id) + assert_equal [5, 6, 7], comments.collect(&:id) assert_no_queries do comments.first.post end end def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name - quoted_posts_id= Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") + quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") assert_nothing_raised do Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4) end @@ -415,7 +415,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end 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") + 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) end @@ -452,8 +452,8 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_has_many_quotes_table_and_column_names michael = Person.all.merge!(includes: :references).find(people(:michael).id) - references(:michael_magician,:michael_unicyclist) - assert_no_queries { assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) } + references(:michael_magician, :michael_unicyclist) + assert_no_queries { assert_equal references(:michael_magician, :michael_unicyclist), michael.references.sort_by(&:id) } end def test_eager_load_has_many_through_quotes_table_and_column_names @@ -464,7 +464,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_has_many_with_string_keys subscriptions = subscriptions(:webster_awdr, :webster_rfr) - subscriber =Subscriber.all.merge!(includes: :subscriptions).find(subscribers(:second).id) + subscriber = Subscriber.all.merge!(includes: :subscriptions).find(subscribers(:second).id) assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end @@ -563,13 +563,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: "posts.body = 'hello'", order: "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect(&:id) + assert_equal [4, 5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: [ "posts.body = ?", "hello" ], order: "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect(&:id) + assert_equal [4, 5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers @@ -739,18 +739,25 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_invalid_association_reference - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.all.merge!(includes: :monkeys ).find(6) + e = assert_raise(ActiveRecord::AssociationNotFoundError) { + Post.all.merge!(includes: :monkeys).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ :monkeys ]).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ "monkeys" ]).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ :monkeys, :elephants ]).find(6) } + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) end def test_eager_has_many_through_with_order @@ -844,7 +851,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def find_all_ordered(className, include=nil) + def find_all_ordered(className, include = nil) className.all.merge!(order: "#{className.table_name}.#{className.primary_key}", includes: include).to_a end @@ -903,8 +910,8 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm # Eager includes of has many and habtm associations aren't necessarily sorted in the same way def assert_equal_after_sort(item1, item2, item3 = nil) - assert_equal(item1.sort { |a,b| a.id <=> b.id }, item2.sort { |a,b| a.id <=> b.id }) - assert_equal(item3.sort { |a,b| a.id <=> b.id }, item2.sort { |a,b| a.id <=> b.id }) if item3 + assert_equal(item1.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id }) + assert_equal(item3.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id }) if item3 end # Test regular association, association with conditions, association with # STI, and association with conditions assured not to be true @@ -933,7 +940,11 @@ class EagerAssociationTest < ActiveRecord::TestCase d2 = find_all_ordered(Firm, :account) d1.each_index do |i| assert_equal(d1[i], d2[i]) - assert_equal(d1[i].account, d2[i].account) + if d1[i].account.nil? + assert_nil(d2[i].account) + else + assert_equal(d1[i].account, d2[i].account) + end end end @@ -943,7 +954,13 @@ class EagerAssociationTest < ActiveRecord::TestCase d2 = find_all_ordered(Client, firm_types) d1.each_index do |i| assert_equal(d1[i], d2[i]) - firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) } + firm_types.each do |type| + if (expected = d1[i].send(type)).nil? + assert_nil(d2[i].send(type)) + else + assert_equal(expected, d2[i].send(type)) + end + end end end def test_eager_with_valid_association_as_string_not_symbol @@ -1077,12 +1094,6 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do - Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author } - - posts = assert_queries(2) do Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts @@ -1163,7 +1174,7 @@ class EagerAssociationTest < ActiveRecord::TestCase expected = Firm.find(1).clients_using_primary_key.sort_by(&:name) # Oracle adapter truncates alias to 30 characters if current_adapter?(:OracleAdapter) - firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0,30]+".name").find(1) + firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0, 30] + ".name").find(1) else firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1) end @@ -1346,6 +1357,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised do authors(:david).essays.includes(:writer).any? authors(:david).essays.includes(:writer).exists? + authors(:david).essays.includes(:owner).where("name IS NOT NULL").exists? end end @@ -1360,7 +1372,7 @@ class EagerAssociationTest < ActiveRecord::TestCase test "including associations with where.not adds implicit references" do author = assert_queries(2) { - Author.includes(:posts).where.not(posts: { title: "Welcome to the weblog" } ).last + Author.includes(:posts).where.not(posts: { title: "Welcome to the weblog" }).last } assert_no_queries { diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index cc86e1a16d..f707a170f5 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -36,6 +36,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent end + def test_extension_with_dirty_target + comment = posts(:welcome).comments.build(body: "New comment") + assert_equal comment, posts(:welcome).comments.with_content("New comment") + end + def test_marshalling_extensions david = developers(:david) assert_equal projects(:action_controller), david.projects.find_most_recent @@ -45,7 +50,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase # Marshaling an association shouldn't make it unusable by wiping its reflection. assert_not_nil david.association(:projects).reflection - david_too = Marshal.load(marshalled) + david_too = Marshal.load(marshalled) assert_equal projects(:action_controller), david_too.projects.find_most_recent end @@ -73,6 +78,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal post.association(:comments), post.comments.where("1=1").the_association end + def test_association_with_default_scope + assert_raises OopsError do + posts(:welcome).comments.destroy_all + end + end + private def extend!(model) 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 06fc7a4388..f73005b3cb 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 @@ -86,6 +86,12 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end +ActiveSupport::Deprecation.silence do + class DeveloperWithConstantClassName < Developer + has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys + end +end + class DeveloperWithExtendOption < Developer module NamedExtension def category @@ -105,6 +111,21 @@ class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base association_foreign_key: "developer_id" end +class Kitchen < ActiveRecord::Base + has_one :sink +end + +class Sink < ActiveRecord::Base + has_and_belongs_to_many :sources, join_table: :edges + belongs_to :kitchen + accepts_nested_attributes_for :kitchen +end + +class Source < ActiveRecord::Base + self.table_name = "men" + has_and_belongs_to_many :sinks, join_table: :edges +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -249,8 +270,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert !p.persisted? assert aredridel.save assert aredridel.persisted? - assert_equal no_of_devels+1, Developer.count - assert_equal no_of_projects+1, Project.count + assert_equal no_of_devels + 1, Developer.count + assert_equal no_of_projects + 1, Project.count assert_equal 2, aredridel.projects.size assert_equal 2, aredridel.projects.reload.size end @@ -346,19 +367,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end - def test_create_by_new_record - devel = Developer.new(name: "Marcel", salary: 75000) - devel.projects.build(name: "Make bed") - proj2 = devel.projects.build(name: "Lie in it") - assert_equal devel.projects.last, proj2 - assert !proj2.persisted? - devel.save - assert devel.persisted? - assert proj2.persisted? - assert_equal devel.projects.last, proj2 - assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated - end - def test_creation_respects_hash_condition # in Oracle '' is saved as null therefore need to save ' ' in not null column post = categories(:general).post_with_conditions.build(body: " ") @@ -379,7 +387,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev.projects << projects(:active_record) assert_equal 3, dev.projects.size - assert_equal 1, dev.projects.distinct.size + assert_equal 1, dev.projects.uniq.size end def test_distinct_before_the_fact @@ -739,8 +747,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped_having - assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size - assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 } + assert_equal 2, projects(:active_record).well_paid_salary_groups.to_a.size + assert projects(:active_record).well_paid_salary_groups.all? { |g| g.salary > 10000 } end def test_get_ids @@ -933,20 +941,22 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_not_nil Developer._reflections["shared_computers"] # Checking the fixture for named association is important here, because it's the only way # we've been able to reproduce this bug - assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers") + assert_not_nil File.read(File.expand_path("../../fixtures/developers.yml", __dir__)).index("shared_computers") assert_equal developers(:david).shared_computers.first, computers(:laptop) end def test_with_symbol_class_name assert_nothing_raised do - DeveloperWithSymbolClassName.new + developer = DeveloperWithSymbolClassName.new + developer.projects end end - def test_association_force_reload_with_only_true_is_deprecated - developer = Developer.find(1) - - assert_deprecated { developer.projects(true) } + def test_with_constant_class_name + assert_nothing_raised do + developer = DeveloperWithConstantClassName.new + developer.projects + end end def test_alternate_database @@ -1000,4 +1010,22 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase user = User.create! assert_nothing_raised { user.jobs_pool.clear } end + + def test_has_and_belongs_to_many_while_partial_writes_false + begin + original_partial_writes = ActiveRecord::Base.partial_writes + ActiveRecord::Base.partial_writes = false + developer = Developer.new(name: "Mehmet Emin İNAÇ") + developer.projects << Project.new(name: "Bounty") + + assert developer.save + ensure + ActiveRecord::Base.partial_writes = original_partial_writes + end + end + + def test_has_and_belongs_to_many_with_belongs_to + sink = Sink.create! kitchen: Kitchen.new, sources: [Source.new] + assert_equal 1, sink.sources.count + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index fed59c2ab3..a794eba691 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -40,7 +40,7 @@ require "models/zine" require "models/interest" class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase - fixtures :authors, :posts, :comments + fixtures :authors, :author_addresses, :posts, :comments def test_should_generate_valid_sql author = authors(:david) @@ -51,7 +51,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa end class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase - fixtures :authors, :essays, :subscribers, :subscriptions, :people + fixtures :authors, :author_addresses, :essays, :subscribers, :subscriptions, :people def test_custom_primary_key_on_new_record_should_fetch_with_query subscriber = Subscriber.new(nick: "webster132") @@ -84,7 +84,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase david = people(:david) assert_equal ["A Modest Proposal"], david.essays.map(&:name) - david.essays = [Essay.create!(name: "Remote Work" )] + david.essays = [Essay.create!(name: "Remote Work")] assert_equal ["Remote Work"], david.essays.map(&:name) end @@ -100,7 +100,7 @@ end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, - :developers_projects, :topics, :authors, :comments, + :developers_projects, :topics, :authors, :author_addresses, :comments, :posts, :readers, :taggings, :cars, :jobs, :tags, :categorizations, :zines, :interests @@ -187,7 +187,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ship.parts.clear part.reload - assert_equal nil, part.ship + assert_nil part.ship assert !part.updated_at_changed? end @@ -528,7 +528,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_should_append_to_association_order - ordered_clients = companies(:first_firm).clients_sorted_desc.order("companies.id") + ordered_clients = companies(:first_firm).clients_sorted_desc.order("companies.id") assert_equal ["id DESC", "companies.id"], ordered_clients.order_values end @@ -611,21 +611,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_update_all_on_association_accessed_before_save firm = Firm.new(name: "Firm") - clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: "Great!") - assert_not_equal clients_proxy_id, firm.clients.object_id end def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key - # We can use the same cached proxy object because the id is available for the scope firm = Firm.new(name: "Firm", id: 100) - clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: "Great!") - assert_equal clients_proxy_id, firm.clients.object_id end def test_belongs_to_sanity @@ -788,6 +783,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id) end + def test_select_with_block_and_dirty_target + assert_equal 2, posts(:welcome).comments.select { true }.size + posts(:welcome).comments.build + assert_equal 3, posts(:welcome).comments.select { true }.size + end + def test_select_without_foreign_key assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit end @@ -912,6 +913,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company.clients_of_firm.build("name" => "Another Client") company.clients_of_firm.build("name" => "Yet Another Client") assert_equal 4, company.clients_of_firm.size + assert_equal 4, company.clients_of_firm.uniq.size end def test_collection_not_empty_after_building @@ -983,7 +985,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_without_loading_association - first_firm = companies(:first_firm) + first_firm = companies(:first_firm) Firm.column_names Client.column_names @@ -1574,26 +1576,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(name: "child") end - def test_restrict_with_error_is_deprecated_using_key_many - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: "message for deprecated key" } } } } - - firm = RestrictedWithErrorFirm.create!(name: "restrict") - firm.companies.create(name: "child") - - assert !firm.companies.empty? - - assert_deprecated { firm.destroy } - - assert !firm.errors.empty? - - assert_equal "message for deprecated key", firm.errors[:base].first - assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.companies.exists?(name: "child") - ensure - I18n.backend.reload! - end - def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") @@ -1735,6 +1717,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !company.clients.loaded? end + def test_counter_cache_on_unloaded_association + car = Car.create(name: "My AppliCar") + assert_equal car.engines.size, 0 + end + def test_get_ids_ignores_include_option assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids end @@ -1916,7 +1903,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_many_on_loaded_association_should_not_use_query firm = companies(:first_firm) - firm.clients.collect # force load + firm.clients.load # force load assert_no_queries { assert firm.clients.many? } end @@ -1955,7 +1942,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_none_on_loaded_association_should_not_use_query firm = companies(:first_firm) - firm.clients.collect # force load + firm.clients.load # force load assert_no_queries { assert ! firm.clients.none? } end @@ -1990,7 +1977,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_one_on_loaded_association_should_not_use_query firm = companies(:first_firm) - firm.clients.collect # force load + firm.clients.load # force load assert_no_queries { assert ! firm.clients.one? } end @@ -2050,12 +2037,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal client_association.new.attributes, client_association.send(:new).attributes end - def test_respond_to_private_class_methods - client_association = companies(:first_firm).clients - assert !client_association.respond_to?(:private_method) - assert client_association.respond_to?(:private_method, true) - end - def test_creating_using_primary_key firm = Firm.all.merge!(order: "id").first client = firm.clients_using_primary_key.create!(name: "test") @@ -2270,7 +2251,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "association with extend option with multiple extensions" do post = posts(:welcome) assert_equal "lifo", post.comments_with_extend_2.author - assert_equal "hello", post.comments_with_extend_2.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting + end + + test "extend option affects per association" do + post = posts(:welcome) + assert_equal "lifo", post.comments_with_extend.author + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hello", post.comments_with_extend.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting end test "delete record with complex joins" do @@ -2291,7 +2280,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 @@ -2460,16 +2449,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end - test "double insertion of new object to association when same association used in the after create callback of a new object" do - car = Car.create! - car.bulbs << TrickyBulb.new - assert_equal 1, car.bulbs.size + 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 } + + car = Car.create! + car.bulbs << Bulb.new + + assert_equal 1, car.bulbs.size + end end - def test_association_force_reload_with_only_true_is_deprecated - company = Company.find(1) + test "prevent double firing the before save callback of new object when the parent association saved in the callback" do + reset_callbacks(:save, Bulb) do + count = 0 + Bulb.before_save { |record| record.car.save && count += 1 } - assert_deprecated { company.clients_of_firm(true) } + car = Car.create! + car.bulbs.create! + + assert_equal 1, count + end end class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base @@ -2510,9 +2510,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_no_queries { car.bulb_ids } end + def test_loading_association_in_validate_callback_doesnt_affect_persistence + reset_callbacks(:validation, Bulb) do + Bulb.after_validation { |record| record.car.bulbs.load } + + car = Car.create!(name: "Car") + bulb = car.bulbs.create! + + assert_equal [bulb], car.bulbs + end + end + private def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.load_target end + + def reset_callbacks(kind, klass) + old_callbacks = {} + old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup + klass.subclasses.each do |subclass| + old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup + end + yield + ensure + klass.send("_#{kind}_callbacks=", old_callbacks[klass]) + klass.subclasses.each do |subclass| + subclass.send("_#{kind}_callbacks=", old_callbacks[subclass]) + end + end end 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 9f716d7820..9156f6d57a 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -28,6 +28,9 @@ require "models/member" require "models/membership" require "models/club" require "models/organization" +require "models/user" +require "models/family" +require "models/family_tree" class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, @@ -61,10 +64,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club1.members.sort_by(&:id) end - def make_model(name) - Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } - end - def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; "Person"; end @@ -75,7 +74,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts = person_prime.includes(:posts).first.posts assert_operator posts.length, :>, 1 - posts.each_cons(2) do |left,right| + posts.each_cons(2) do |left, right| assert_operator left.id, :>, right.id end end @@ -149,20 +148,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert after_destroy_called, "after destroy should be called" end - def make_no_pk_hm_t - lesson = make_model "Lesson" - student = make_model "Student" - - lesson_student = make_model "LessonStudent" - lesson_student.table_name = "lessons_students" - - lesson_student.belongs_to :lesson, anonymous_class: lesson - lesson_student.belongs_to :student, anonymous_class: student - lesson.has_many :lesson_students, anonymous_class: lesson_student - lesson.has_many :students, through: :lesson_students, anonymous_class: student - [lesson, lesson_student, student] - end - def test_pk_is_not_required_for_join post = Post.includes(:scategories).first post2 = Post.includes(:categories).first @@ -402,7 +387,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - assert_equal nil, reference.reload.job_id + assert_nil reference.reload.job_id ensure Reference.make_comments = false end @@ -423,7 +408,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end # Check that the destroy callback on Reference did not run - assert_equal nil, person.reload.comments + assert_nil person.reload.comments ensure Reference.make_comments = false end @@ -485,7 +470,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end references.each do |reference| - assert_equal nil, reference.reload.job_id + assert_nil reference.reload.job_id end end @@ -702,7 +687,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :after, "Bob"], [:added, :before, "Lary"], [:added, :after, "Lary"] - ],log.last(6) + ], log.last(6) post.people_with_callbacks.build(first_name: "Ted") assert_equal [ @@ -716,7 +701,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :after, "Sam"] ], log.last(2) - post.people_with_callbacks = [people(:michael),people(:david), Person.new(first_name: "Julian"), Person.create!(first_name: "Roger")] + post.people_with_callbacks = [people(:michael), people(:david), Person.new(first_name: "Julian"), Person.create!(first_name: "Roger")] assert_equal((%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort) assert_equal [ [:added, :before, "Julian"], @@ -880,13 +865,34 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase book.subscriber_ids = [] assert_equal [], book.subscribers.reload end + end + def test_collection_singular_ids_setter_with_changed_primary_key + company = companies(:first_firm) + client = companies(:first_client) + company.clients_using_primary_key_ids = [client.name] + assert_equal [client], company.clients_using_primary_key end def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) - ids = [Developer.first.id, -9999] - assert_raises(ActiveRecord::AssociationTypeMismatch) { company.developer_ids= ids } + ids = [Developer.first.id, -9999] + e = assert_raises(ActiveRecord::RecordNotFound) { company.developer_ids = ids } + assert_match(/Couldn't find all Developers with 'id'/, e.message) + end + + def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set_with_changed_primary_key + company = companies(:first_firm) + ids = [Client.first.name, "unknown client"] + e = assert_raises(ActiveRecord::RecordNotFound) { company.clients_using_primary_key_ids = ids } + assert_match(/Couldn't find all Clients with 'name'/, e.message) + end + + def test_collection_singular_ids_through_setter_raises_exception_when_invalid_ids_set + author = authors(:david) + ids = [categories(:general).name, "Unknown"] + e = assert_raises(ActiveRecord::RecordNotFound) { author.essay_category_ids = ids } + assert_equal "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2)", e.message end def test_build_a_model_from_hm_through_association_with_where_clause @@ -1182,12 +1188,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_nil Club.new.special_favourites.distinct_value end - def test_association_force_reload_with_only_true_is_deprecated - post = Post.find(1) - - assert_deprecated { post.people(true) } - end - def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes member = Member.create! club = Club.create! @@ -1215,4 +1215,42 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ensure TenantMembership.current_member = nil end + + def test_has_many_through_with_scope_should_respect_table_alias + family = Family.create! + users = 3.times.map { User.create! } + FamilyTree.create!(member: users[0], family: family) + FamilyTree.create!(member: users[1], family: family) + FamilyTree.create!(member: users[2], family: family, token: "wat") + + assert_equal 2, users[0].family_members.to_a.size + assert_equal 0, users[2].family_members.to_a.size + end + + def test_incorrectly_ordered_through_associations + assert_raises(ActiveRecord::HasManyThroughOrderError) do + DeveloperWithIncorrectlyOrderedHasManyThrough.create( + companies: [Company.create] + ) + end + end + + private + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def make_no_pk_hm_t + lesson = make_model "Lesson" + student = make_model "Student" + + lesson_student = make_model "LessonStudent" + lesson_student.table_name = "lessons_students" + + lesson_student.belongs_to :lesson, anonymous_class: lesson + lesson_student.belongs_to :student, anonymous_class: student + lesson.has_many :lesson_students, anonymous_class: lesson_student + lesson.has_many :students, through: :lesson_students, anonymous_class: student + [lesson, lesson_student, student] + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 1a0e6d2f8e..7c11d2e7fc 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -186,25 +186,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end - def test_restrict_with_error_is_deprecated_using_key_one - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: "message for deprecated key" } } } } - - firm = RestrictedWithErrorFirm.create!(name: "restrict") - firm.create_account(credit_limit: 10) - - assert_not_nil firm.account - - assert_deprecated { firm.destroy } - - assert !firm.errors.empty? - assert_equal "message for deprecated key", firm.errors[:base].first - assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.account.present? - ensure - I18n.backend.reload! - end - def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.create_account(credit_limit: 10) @@ -326,6 +307,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_reload_association + odegy = companies(:odegy) + + assert_equal 53, odegy.account.credit_limit + Account.where(id: odegy.account.id).update_all(credit_limit: 80) + assert_equal 53, odegy.account.credit_limit + + assert_equal 80, odegy.reload_account.credit_limit + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -485,7 +476,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id - assert_equal "Failed to remove the existing associated ship. " + + assert_equal "Failed to remove the existing associated ship. " \ "The record failed to save after its foreign key was set to nil.", error.message end @@ -601,7 +592,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase new_ship = Ship.create(name: "new name") assert_queries(2) do - # One query for updating name and second query for updating pirate_id + # One query to nullify the old ship, one query to update the new ship pirate.ship = new_ship end @@ -654,15 +645,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end - def test_association_force_reload_with_only_true_is_deprecated - firm = Firm.find(1) - - assert_deprecated { firm.account(true) } - end - class SpecialBook < ActiveRecord::Base self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" + has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" end class SpecialAuthor < ActiveRecord::Base @@ -670,11 +656,27 @@ class HasOneAssociationsTest < ActiveRecord::TestCase has_one :book, class_name: "SpecialBook", foreign_key: "author_id" end - def test_assocation_enum_works_properly + class SpecialSupscription < ActiveRecord::Base + self.table_name = "subscriptions" + belongs_to :book, class_name: "SpecialBook" + end + + def test_association_enum_works_properly author = SpecialAuthor.create!(name: "Test") book = SpecialBook.create!(status: "published") author.book = book - refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" } ).count + refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + end + + def test_association_enum_works_properly_with_nested_join + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(status: "published") + author.book = book + + where_clause = { books: { subscriptions: { subscriber_id: nil } } } + assert_nothing_raised do + SpecialAuthor.joins(book: :subscription).where.not(where_clause) + end end end 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 b2f47d2daf..28b883586d 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -23,7 +23,7 @@ require "models/customer_carrier" class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, - :dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners + :dashboards, :speedometers, :authors, :author_addresses, :posts, :comments, :categories, :essays, :owners def setup @member = members(:groucho) @@ -82,10 +82,17 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_set_record_to_nil_should_delete_association @member.club = nil @member.reload - assert_equal nil, @member.current_membership + assert_nil @member.current_membership assert_nil @member.club end + def test_set_record_after_delete_association + @member.club = nil + @member.club = clubs(:moustache_club) + @member.reload + assert_equal clubs(:moustache_club), @member.club + end + def test_has_one_through_polymorphic assert_equal clubs(:moustache_club), @member.sponsor_club end @@ -110,12 +117,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase # conditions on the through table assert_equal clubs(:moustache_club), Member.all.merge!(includes: :favourite_club).find(@member.id).favourite_club memberships(:membership_of_favourite_club).update_columns(favourite: false) - assert_equal nil, Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club + assert_nil Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club # conditions on the source table assert_equal clubs(:moustache_club), Member.all.merge!(includes: :hairy_club).find(@member.id).hairy_club clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons") - assert_equal nil, Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club + assert_nil Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club end def test_has_one_through_polymorphic_with_source_type @@ -123,7 +130,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_eager_has_one_through_polymorphic_with_source_type - clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?","Moustache and Eyebrow Fancier Club"]).to_a + clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?", "Moustache and Eyebrow Fancier Club"]).to_a # Only the eyebrow fanciers club has a sponsored_member assert_not_nil assert_no_queries { clubs[0].sponsored_member } end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 7414869c8f..ddf5bc6f0b 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -10,7 +10,7 @@ require "models/tagging" require "models/tag" class InnerJoinAssociationTest < ActiveRecord::TestCase - fixtures :authors, :essays, :posts, :comments, :categories, :categories_posts, :categorizations, + fixtures :authors, :author_addresses, :essays, :posts, :comments, :categories, :categories_posts, :categorizations, :taggings, :tags def test_construct_finder_sql_applies_aliases_tables_on_association_conditions diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 6fe6ee6783..467cc73ecd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -443,7 +443,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held" assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" - assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" + assert_nil man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" man.name = "Ben Bitdiddle" assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed" man.interests.find(interest.id).man.name = "Alyssa P. Hacker" @@ -651,20 +651,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end - def test_child_instance_should_be_shared_with_replaced_via_method_parent - face = faces(:confused) - new_man = Man.new - - assert_not_nil face.polymorphic_man - face.polymorphic_man = new_man - - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance" - face.description = "Bongo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance" - new_man.polymorphic_face.description = "Mungo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" - end - def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed new_man = Man.new face = Face.new diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 15a7ae941a..c078cef064 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -19,7 +19,7 @@ require "models/car" class AssociationsJoinModelTest < ActiveRecord::TestCase self.use_transactional_tests = false unless supports_savepoints? - fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, + fixtures :posts, :authors, :author_addresses, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, # Reload edges table from fixtures as otherwise repeated test was failing :edges @@ -155,21 +155,21 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = posts(:welcome).taggings.count tagging = posts(:welcome).taggings.create(tag: tags(:misc)) assert_equal "Post", tagging.taggable_type - assert_equal old_count+1, posts(:welcome).taggings.count + assert_equal old_count + 1, posts(:welcome).taggings.count end def test_create_bang_polymorphic_with_has_many_scope old_count = posts(:welcome).taggings.count tagging = posts(:welcome).taggings.create!(tag: tags(:misc)) assert_equal "Post", tagging.taggable_type - assert_equal old_count+1, posts(:welcome).taggings.count + assert_equal old_count + 1, posts(:welcome).taggings.count end def test_create_polymorphic_has_one_with_scope old_count = Tagging.count tagging = posts(:welcome).create_tagging(tag: tags(:misc)) assert_equal "Post", tagging.taggable_type - assert_equal old_count+1, Tagging.count + assert_equal old_count + 1, Tagging.count end def test_delete_polymorphic_has_many_with_delete_all @@ -179,7 +179,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy - assert_equal old_count-1, Tagging.count + assert_equal old_count - 1, Tagging.count assert_equal 0, posts(:welcome).taggings.count end @@ -190,7 +190,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy - assert_equal old_count-1, Tagging.count + assert_equal old_count - 1, Tagging.count assert_equal 0, posts(:welcome).taggings.count end @@ -212,7 +212,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy - assert_equal old_count-1, Tagging.count + assert_equal old_count - 1, Tagging.count posts(:welcome).association(:tagging).reload assert_nil posts(:welcome).tagging end @@ -402,7 +402,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_one - assert_equal Tagging.find(1,2).sort_by(&:id), authors(:david).taggings_2 + assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2 end def test_has_many_through_polymorphic_has_many @@ -413,7 +413,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) + assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id) end end @@ -421,7 +421,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase author = Author.all.merge!(where: ["name = ?", "David"], includes: :comments, order: "comments.id").first SpecialComment.new; VerySpecialComment.new assert_no_queries do - assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) + assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], author.comments.collect(&:id) end end @@ -493,25 +493,25 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase push = Tag.create!(name: "pushme") post_thinking = posts(:thinking) assert_nothing_raised { post_thinking.tags << push } - assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, + assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, message = "Expected a Tag in tags collection, got #{wrong.class}.") - assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, + assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags.reload.size) assert_kind_of Tag, post_thinking.tags.create!(name: "foo") - assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, + assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, message = "Expected a Tag in tags collection, got #{wrong.class}.") - assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, + assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags.reload.size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(name: "abc"), Tag.create!(name: "def")) } - assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, + assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, message = "Expected a Tag in tags collection, got #{wrong.class}.") - assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, + assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags.reload.size) 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 2cc6468827..6d3757f467 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -5,10 +5,9 @@ require "models/author" require "models/essay" require "models/categorization" require "models/person" -require "active_support/core_ext/regexp" class LeftOuterJoinAssociationTest < ActiveRecord::TestCase - fixtures :authors, :essays, :posts, :comments, :categorizations, :people + fixtures :authors, :author_addresses, :essays, :posts, :comments, :categorizations, :people def test_construct_finder_sql_applies_aliases_tables_on_association_conditions result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index dc26f6a383..67ff7355b3 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -24,7 +24,7 @@ require "models/membership" require "models/essay" class NestedThroughAssociationsTest < ActiveRecord::TestCase - fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, + fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details, :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts, :categorizations, :memberships, :essays diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index f8b686721e..45e1803858 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -22,14 +22,21 @@ class RequiredAssociationsTest < ActiveRecord::TestCase @connection.drop_table "children", if_exists: true end - test "belongs_to associations are not required by default" do - model = subclass_of(Child) do - belongs_to :parent, inverse_of: false, - class_name: "RequiredAssociationsTest::Parent" - end + test "belongs_to associations can be optional by default" do + begin + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = false + + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end - assert model.new.save - assert model.new(parent: Parent.new).save + assert model.new.save + assert model.new(parent: Parent.new).save + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end end test "required belongs_to associations have presence validated" do @@ -46,6 +53,27 @@ class RequiredAssociationsTest < ActiveRecord::TestCase assert record.save end + test "belongs_to associations can be required by default" do + begin + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent must exist"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + end + test "has_one associations are not required by default" do model = subclass_of(Parent) do has_one :child, inverse_of: false, diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index c095b3a91c..2eb31326a5 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -22,7 +22,7 @@ require "models/interest" class AssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :developers_projects, - :computers, :people, :readers, :authors, :author_favorites + :computers, :people, :readers, :authors, :author_addresses, :author_favorites def test_eager_loading_should_not_change_count_of_children liquid = Liquid.create(name: "salty") @@ -88,10 +88,10 @@ class AssociationsTest < ActiveRecord::TestCase assert firm.clients.empty?, "New firm should have cached no client objects" assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count" - ActiveSupport::Deprecation.silence do - assert !firm.clients(true).empty?, "New firm should have reloaded client objects" - assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" - end + firm.clients.reload + + assert !firm.clients.empty?, "New firm should have reloaded client objects" + assert_equal 1, firm.clients.size, "New firm should have reloaded clients count" end def test_using_limitable_reflections_helper @@ -104,19 +104,6 @@ class AssociationsTest < ActiveRecord::TestCase assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass" end - def test_force_reload_is_uncached - firm = Firm.create!("name" => "A New Firm, Inc") - Client.create!("name" => "TheClient.com", :firm => firm) - - ActiveSupport::Deprecation.silence do - ActiveRecord::Base.cache do - firm.clients.each {} - assert_queries(0) { assert_not_nil firm.clients.each {} } - assert_queries(1) { assert_not_nil firm.clients(true).each {} } - end - end - end - def test_association_with_references firm = companies(:first_firm) assert_includes firm.association_with_references.references_values, "foo" @@ -124,7 +111,7 @@ class AssociationsTest < ActiveRecord::TestCase end class AssociationProxyTest < ActiveRecord::TestCase - fixtures :authors, :posts, :categorizations, :categories, :developers, :projects, :developers_projects + fixtures :authors, :author_addresses, :posts, :categorizations, :categories, :developers, :projects, :developers_projects def test_push_does_not_load_target david = authors(:david) @@ -235,7 +222,14 @@ class AssociationProxyTest < ActiveRecord::TestCase test "proxy object is cached" do david = developers(:david) - assert david.projects.equal?(david.projects) + assert_same david.projects, david.projects + end + + test "proxy object can be stubbed" do + david = developers(:david) + david.projects.define_singleton_method(:extra_method) { 42 } + + assert_equal 42, david.projects.extra_method end test "inverses get set of subsets of the association" do diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index a8592bd179..1fc63a49d4 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord module AttributeMethods class ReadTest < ActiveRecord::TestCase - class FakeColumn < Struct.new(:name) + FakeColumn = Struct.new(:name) do def type; :integer; end end @@ -14,6 +14,7 @@ module ActiveRecord def self.decorate_matching_attribute_types(*); end def self.initialize_generated_modules; end + include ActiveRecord::DefineCallbacks include ActiveRecord::AttributeMethods def self.attribute_names diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 4c77ecab7c..4d24a980dc 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -92,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "attribute keys on a new instance" do t = Topic.new - assert_equal nil, t.title, "The topics table has a title column, so it should be nil" + assert_nil t.title, "The topics table has a title column, so it should be nil" assert_raise(NoMethodError) { t.title2 } end @@ -156,7 +156,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase keyboard = Keyboard.create keyboard.key_number = "10" assert_equal "10", keyboard.id_before_type_cast - assert_equal nil, keyboard.read_attribute_before_type_cast("id") + assert_nil keyboard.read_attribute_before_type_cast("id") assert_equal "10", keyboard.read_attribute_before_type_cast("key_number") assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number) end @@ -213,7 +213,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase record.written_on = "345643456" assert_equal "345643456", record.written_on_before_type_cast - assert_equal nil, record.written_on + assert_nil record.written_on record.written_on = "2009-10-11 12:13:14" assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast @@ -294,7 +294,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = Topic.new(new_topic) assert_equal new_topic[:title], topic.title - topic.attributes= new_topic_values + topic.attributes = new_topic_values assert_equal new_topic_values[:title], topic.title end @@ -319,6 +319,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "Still another topic: part 4", topic.title end + test "write_attribute can write aliased attributes as well" do + topic = Topic.new(title: "Don't change the topic") + topic.write_attribute :heading, "New topic" + + assert_equal "New topic", topic.title + end + test "read_attribute" do topic = Topic.new topic.title = "Don't change the topic" @@ -329,6 +336,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "Don't change the topic", topic[:title] end + test "read_attribute can read aliased attributes as well" do + topic = Topic.new(title: "Don't change the topic") + + assert_equal "Don't change the topic", topic.read_attribute("heading") + assert_equal "Don't change the topic", topic["heading"] + + assert_equal "Don't change the topic", topic.read_attribute(:heading) + assert_equal "Don't change the topic", topic[:heading] + end + test "read_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do computer = Computer.select("id").first assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] } @@ -609,7 +626,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase utc_time = Time.utc(2008, 1, 1) cst_time = utc_time.in_time_zone("Central Time (US & Canada)") in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = cst_time assert_equal utc_time, record.written_on assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone @@ -633,7 +650,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase (-11..13).each do |timezone_offset| time_string = utc_time.in_time_zone(timezone_offset).to_s in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = time_string assert_equal Time.zone.parse(time_string), record.written_on assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone @@ -654,7 +671,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "setting a time zone-aware attribute to a blank string returns nil" do in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = " " assert_nil record.written_on assert_nil record[:written_on] @@ -665,7 +682,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase time_string = "Tue Jan 01 00:00:00 2008" (-11..13).each do |timezone_offset| in_time_zone timezone_offset do - record = @target.new + record = @target.new record.written_on = time_string assert_equal Time.zone.parse(time_string), record.written_on assert_equal ActiveSupport::TimeZone[timezone_offset], record.written_on.time_zone @@ -677,7 +694,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "setting a time zone-aware datetime in the current time zone" do utc_time = Time.utc(2008, 1, 1) in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = utc_time.in_time_zone assert_equal utc_time, record.written_on assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone @@ -737,7 +754,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "time zone-aware attributes do not recurse infinitely on invalid values" do in_time_zone "Pacific Time (US & Canada)" do record = @target.new(bonus_time: []) - assert_equal nil, record.bonus_time + assert_nil record.bonus_time end end @@ -849,6 +866,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert subklass.method_defined?(:id), "subklass is missing id method" end + test "define_attribute_method works with both symbol and string" do + klass = Class.new(ActiveRecord::Base) + + assert_nothing_raised { klass.define_attribute_method(:foo) } + assert_nothing_raised { klass.define_attribute_method("bar") } + end + test "read_attribute with nil should not asplode" do assert_nil Topic.new.read_attribute(nil) end diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 059b5b2401..bd4b200735 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -25,7 +25,7 @@ module ActiveRecord attributes = builder.build_from_database(foo: "3.3") assert_equal "3.3", attributes[:foo].value_before_type_cast - assert_equal nil, attributes[:bar].value_before_type_cast + assert_nil attributes[:bar].value_before_type_cast assert_equal :bar, attributes[:bar].name end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index f4620ae2da..3705a6be89 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -255,5 +255,13 @@ module ActiveRecord assert_includes inspection, "non_existent_decimal" end + + test "attributes do not require a type" do + klass = Class.new(OverloadedType) do + attribute :no_type + end + assert_equal 1, klass.new(no_type: 1).no_type + assert_equal "foo", klass.new(no_type: "foo").no_type + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index c24d7b8835..2203aa1788 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -36,11 +36,11 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase private - def should_be_cool - unless self.first_name == "cool" - errors.add :first_name, "not cool" + def should_be_cool + unless first_name == "cool" + errors.add :first_name, "not cool" + end end - end } reference = Class.new(ActiveRecord::Base) { self.table_name = "references" @@ -792,6 +792,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end @ship.pirate.catchphrase = "Changed Catchphrase" + @ship.name_will_change! assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship @@ -1130,7 +1131,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase assert_queries(0) { @ship.save! } @parrot = @pirate.parrots.create(name: "some_name") - @parrot.name="changed_name" + @parrot.name = "changed_name" assert_queries(1) { @ship.save! } assert_queries(0) { @ship.save! } end @@ -1390,6 +1391,14 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal "Squawky", parrot.reload.name end + def test_should_not_update_children_when_parent_creation_with_no_reason + parrot = Parrot.create!(name: "Polly") + assert_equal 0, parrot.updated_count + + Pirate.create!(parrot_ids: [parrot.id], catchphrase: "Arrrr") + assert_equal 0, parrot.reload.updated_count + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = "" } @@ -1698,3 +1707,27 @@ class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase assert_nothing_raised { invoice.line_items.create(amount: 10) } end end + +class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :comments, inverse_of: :post + end + + class Comment < ActiveRecord::Base + belongs_to :post, inverse_of: :comments + + attr_accessor :post_comments_count + after_save do + self.post_comments_count = post.comments.count + end + end + + def test_after_save_callback_with_autosave + post = Post.new(title: "Test", body: "...") + comment = post.comments.build(body: "...") + post.save! + + assert_equal 1, post.comments.count + assert_equal 1, comment.post_comments_count + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a2132bb577..dc32e995a4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -4,6 +4,7 @@ require "models/author" require "models/topic" require "models/reply" require "models/category" +require "models/categorization" require "models/company" require "models/customer" require "models/developer" @@ -33,8 +34,6 @@ class SecondAbstractClass < FirstAbstractClass self.abstract_class = true end class Photo < SecondAbstractClass; end -class Category < ActiveRecord::Base; end -class Categorization < ActiveRecord::Base; end class Smarts < ActiveRecord::Base; end class CreditCard < ActiveRecord::Base class PinNumber < ActiveRecord::Base @@ -45,8 +44,6 @@ class CreditCard < ActiveRecord::Base class Brand < Category; end end class MasterCreditCard < ActiveRecord::Base; end -class Post < ActiveRecord::Base; end -class Computer < ActiveRecord::Base; end class NonExistentTable < ActiveRecord::Base; end class TestOracleDefault < ActiveRecord::Base; end @@ -56,12 +53,6 @@ end class Weird < ActiveRecord::Base; end -class Boolean < ActiveRecord::Base - def has_fun - super - end -end - class LintTest < ActiveRecord::TestCase include ActiveModel::Lint::Tests @@ -73,7 +64,7 @@ class LintTest < ActiveRecord::TestCase end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :categorizations, :categories, :posts + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts def test_column_names_are_escaped conn = ActiveRecord::Base.connection @@ -107,12 +98,11 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end - unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter) - def test_limit_with_comma - assert_deprecated do - assert Topic.limit("1,2").to_a - end - end + def test_primary_key_and_references_columns_should_be_identical_type + pk = Author.columns_hash["id"] + ref = Post.columns_hash["author_id"] + + assert_equal pk.bigint?, ref.bigint? end def test_many_mutations @@ -144,10 +134,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_limit_should_sanitize_sql_injection_for_limit_with_commas - assert_deprecated do - assert_raises(ArgumentError) do - Topic.limit("1, 7 procedure help()").to_a - end + assert_raises(ArgumentError) do + Topic.limit("1, 7 procedure help()").to_a end end @@ -494,12 +482,12 @@ class BasicsTest < ActiveRecord::TestCase def test_utc_as_time_zone_and_new with_timezone_config default: :utc do - attributes = { "bonus_time(1i)"=>"2000", - "bonus_time(2i)"=>"1", - "bonus_time(3i)"=>"1", - "bonus_time(4i)"=>"10", - "bonus_time(5i)"=>"35", - "bonus_time(6i)"=>"50" } + attributes = { "bonus_time(1i)" => "2000", + "bonus_time(2i)" => "1", + "bonus_time(3i)" => "1", + "bonus_time(4i)" => "10", + "bonus_time(5i)" => "35", + "bonus_time(6i)" => "50" } topic = Topic.new(attributes) assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time end @@ -622,7 +610,7 @@ class BasicsTest < ActiveRecord::TestCase Topic.find(topic.id).destroy end - assert_equal nil, Topic.find_by_id(topic.id) + assert_nil Topic.find_by_id(topic.id) end def test_comparison_with_different_objects @@ -715,6 +703,17 @@ class BasicsTest < ActiveRecord::TestCase assert_nil topic.bonus_time end + def test_attributes + category = Category.new(name: "Ruby") + + expected_attributes = category.attribute_names.map do |attribute_name| + [attribute_name, category.public_send(attribute_name)] + end.to_h + + assert_instance_of Hash, category.attributes + assert_equal expected_attributes, category.attributes + end + def test_boolean b_nil = Boolean.create("value" => nil) nil_id = b_nil.id @@ -898,7 +897,7 @@ class BasicsTest < ActiveRecord::TestCase # fixed dates / times assert_equal Date.new(2004, 1, 1), default.fixed_date - assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time + assert_equal Time.local(2004, 1, 1, 0, 0, 0, 0), default.fixed_time # char types assert_equal "Y", default.char1 @@ -1035,7 +1034,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_set_table_name_with_inheritance - k = Class.new( ActiveRecord::Base ) + k = Class.new(ActiveRecord::Base) def k.name; "Foo"; end def k.table_name; super + "ks"; end assert_equal "foosks", k.table_name @@ -1086,7 +1085,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_last - last = Developer.last + last = Developer.last assert_equal last, Developer.all.merge!(order: "id desc").first end @@ -1105,17 +1104,17 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_ordered_last - last = Developer.all.merge!(order: "developers.salary ASC").last + last = Developer.all.merge!(order: "developers.salary ASC").last assert_equal last, Developer.all.merge!(order: "developers.salary ASC").to_a.last end def test_find_reverse_ordered_last - last = Developer.all.merge!(order: "developers.salary DESC").last + last = Developer.all.merge!(order: "developers.salary DESC").last assert_equal last, Developer.all.merge!(order: "developers.salary DESC").to_a.last end def test_find_multiple_ordered_last - last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last + last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last assert_equal last, Developer.all.merge!(order: "developers.name, developers.salary DESC").to_a.last end @@ -1130,7 +1129,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_symbol_ordered_last - last = Developer.all.merge!(order: :salary).last + last = Developer.all.merge!(order: :salary).last assert_equal last, Developer.all.merge!(order: :salary).to_a.last end @@ -1210,7 +1209,7 @@ class BasicsTest < ActiveRecord::TestCase def test_marshal_round_trip expected = posts(:welcome) marshalled = Marshal.dump(expected) - actual = Marshal.load(marshalled) + actual = Marshal.load(marshalled) assert_equal expected.attributes, actual.attributes end @@ -1305,12 +1304,6 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_uniq_delegates_to_scoped - assert_deprecated do - assert_equal Bird.all.distinct, Bird.uniq - end - end - def test_distinct_delegates_to_scoped assert_equal Bird.all.distinct, Bird.distinct end @@ -1354,6 +1347,16 @@ class BasicsTest < ActiveRecord::TestCase assert_nil hash["firm_name"] end + def test_slice_accepts_array_argument + attrs = { + title: "slice", + author_name: "@Cohen-Carlisle", + content: "accept arrays so I don't have to splat" + }.with_indifferent_access + topic = Topic.new(attrs) + assert_equal attrs, topic.slice(attrs.keys) + end + def test_default_values_are_deeply_dupped company = Company.new company.description << "foo" diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index f7e21faf0f..fbc3fbb44f 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -35,12 +35,10 @@ class EachTest < ActiveRecord::TestCase end end - if Enumerator.method_defined? :size - def test_each_should_return_a_sized_enumerator - assert_equal 11, Post.find_each(batch_size: 1).size - assert_equal 5, Post.find_each(batch_size: 2, start: 7).size - assert_equal 11, Post.find_each(batch_size: 10_000).size - end + def test_each_should_return_a_sized_enumerator + assert_equal 11, Post.find_each(batch_size: 1).size + assert_equal 5, Post.find_each(batch_size: 2, start: 7).size + assert_equal 11, Post.find_each(batch_size: 10_000).size end def test_each_enumerator_should_execute_one_query_per_batch @@ -145,7 +143,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do Post.find_in_batches(batch_size: 1) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first @@ -410,7 +408,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do Post.in_batches(of: 1) do |relation| assert_kind_of ActiveRecord::Relation, relation assert_kind_of Post, relation.first @@ -515,14 +513,12 @@ class EachTest < ActiveRecord::TestCase assert_equal 2, person.reload.author_id # incremented only once end - if Enumerator.method_defined? :size - def test_find_in_batches_should_return_a_sized_enumerator - assert_equal 11, Post.find_in_batches(batch_size: 1).size - assert_equal 6, Post.find_in_batches(batch_size: 2).size - assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size - assert_equal 4, Post.find_in_batches(batch_size: 3).size - assert_equal 1, Post.find_in_batches(batch_size: 10_000).size - end + def test_find_in_batches_should_return_a_sized_enumerator + assert_equal 11, Post.find_in_batches(batch_size: 1).size + assert_equal 6, Post.find_in_batches(batch_size: 2).size + assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size + assert_equal 4, Post.find_in_batches(batch_size: 3).size + assert_equal 1, Post.find_in_batches(batch_size: 10_000).size end [true, false].each do |load| diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 98d202dd79..5af44c27eb 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -3,36 +3,35 @@ require "models/topic" require "models/author" require "models/post" -module ActiveRecord - class BindParameterTest < ActiveRecord::TestCase - fixtures :topics, :authors, :posts +if ActiveRecord::Base.connection.prepared_statements + module ActiveRecord + class BindParameterTest < ActiveRecord::TestCase + fixtures :topics, :authors, :author_addresses, :posts - class LogListener - attr_accessor :calls + class LogListener + attr_accessor :calls - def initialize - @calls = [] - end + def initialize + @calls = [] + end - def call(*args) - calls << args + def call(*args) + calls << args + end end - end - def setup - super - @connection = ActiveRecord::Base.connection - @subscriber = LogListener.new - @pk = Topic.columns_hash[Topic.primary_key] - @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) - end + def setup + super + @connection = ActiveRecord::Base.connection + @subscriber = LogListener.new + @pk = Topic.columns_hash[Topic.primary_key] + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) + end - teardown do - ActiveSupport::Notifications.unsubscribe(@subscription) - end + def teardown + ActiveSupport::Notifications.unsubscribe(@subscription) + end - if ActiveRecord::Base.connection.supports_statement_cache? && - ActiveRecord::Base.connection.prepared_statements def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: "David") scope = Author.from(subquery, "authors").where(id: 1) @@ -40,9 +39,8 @@ module ActiveRecord end def test_binds_are_logged - sub = Arel::Nodes::BindParam.new - binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] - sql = "select * from topics where id = #{sub.to_sql}" + binds = [bind_attribute("id", 1)] + sql = "select * from topics where id = #{bind_param.to_sql}" @connection.exec_query(sql, "SQL", binds) @@ -56,43 +54,52 @@ module ActiveRecord assert message, "expected a message with binds" end - def test_logs_bind_vars_after_type_cast - binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - payload = { - name: "SQL", - sql: "select * from topics where id = ?", - binds: binds, - type_casted_binds: type_casted_binds - } - event = ActiveSupport::Notifications::Event.new( - "foo", - Time.now, - Time.now, - 123, - payload) - - logger = Class.new(ActiveRecord::LogSubscriber) { - attr_reader :debugs - def initialize - super - @debugs = [] - end - - def debug(str) - @debugs << str - end - }.new - - logger.sql event - assert_match([[@pk.name, 10]].inspect, logger.debugs.first) + def test_logs_binds_after_type_cast + binds = [bind_attribute("id", "10", Type::Integer.new)] + assert_logs_binds(binds) end - private + def test_logs_legacy_binds_after_type_cast + binds = [[@pk, "10"]] + assert_logs_binds(binds) + end - def type_cast(value) - ActiveRecord::Base.connection.type_cast(value) + def test_deprecate_supports_statement_cache + assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? } end + + private + def assert_logs_binds(binds) + payload = { + name: "SQL", + sql: "select * from topics where id = ?", + binds: binds, + type_casted_binds: @connection.type_casted_binds(binds) + } + + event = ActiveSupport::Notifications::Event.new( + "foo", + Time.now, + Time.now, + 123, + payload) + + logger = Class.new(ActiveRecord::LogSubscriber) { + attr_reader :debugs + + def initialize + super + @debugs = [] + end + + def debug(str) + @debugs << str + end + }.new + + logger.sql(event) + assert_match([[@pk.name, 10]].inspect, logger.debugs.first) + end end end end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index bb2829b3c1..7b8264e6e8 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -4,22 +4,48 @@ module ActiveRecord class CacheKeyTest < ActiveRecord::TestCase self.use_transactional_tests = false - class CacheMe < ActiveRecord::Base; end + class CacheMe < ActiveRecord::Base + self.cache_versioning = false + end + + class CacheMeWithVersion < ActiveRecord::Base + self.cache_versioning = true + end setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:cache_mes) { |t| t.timestamps } + @connection.create_table(:cache_mes, force: true) { |t| t.timestamps } + @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps } end teardown do @connection.drop_table :cache_mes, if_exists: true + @connection.drop_table :cache_me_with_versions, if_exists: true end - test "test_cache_key_format_is_not_too_precise" do + test "cache_key format is not too precise" do record = CacheMe.create key = record.cache_key assert_equal key, record.reload.cache_key end + + test "cache_key has no version when versioning is on" do + record = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key + 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? + end + + test "cache_key_with_version always has both key and version" do + r1 = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + + r2 = CacheMe.create + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 7e7076196f..21c5c0efee 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -14,9 +14,9 @@ require "models/speedometer" require "models/ship_part" require "models/treasure" require "models/developer" +require "models/post" require "models/comment" require "models/rating" -require "models/post" class CalculationsTest < ActiveRecord::TestCase fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books @@ -85,14 +85,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_group_by_field c = Account.group(:firm_id).sum(:credit_limit) - [1,6,2].each do |firm_id| + [1, 6, 2].each do |firm_id| assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}" end end def test_should_group_by_arel_attribute c = Account.group(Account.arel_table[:firm_id]).sum(:credit_limit) - [1,6,2].each do |firm_id| + [1, 6, 2].each do |firm_id| assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}" end end @@ -159,14 +159,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_limit_should_apply_before_count - accounts = Account.limit(3).where("firm_id IS NOT NULL") + accounts = Account.limit(4) assert_equal 3, accounts.count(:firm_id) assert_equal 3, accounts.select(:firm_id).count end def test_limit_should_apply_before_count_arel_attribute - accounts = Account.limit(3).where("firm_id IS NOT NULL") + accounts = Account.limit(4) firm_id_attribute = Account.arel_table[:firm_id] assert_equal 3, accounts.count(firm_id_attribute) @@ -220,6 +220,20 @@ class CalculationsTest < ActiveRecord::TestCase assert_match "credit_limit, firm_name", e.message end + def test_apply_distinct_in_count + queries = assert_sql do + Account.distinct.count + Account.group(:firm_id).distinct.count + end + + queries.each do |query| + # `table_alias_length` in `column_alias_for` would execute + # "SHOW max_identifier_length" statement in PostgreSQL adapter. + next if query == "SHOW max_identifier_length" + assert_match %r{\ASELECT(?! DISTINCT) COUNT\(DISTINCT\b}, query + end + end + def test_should_group_by_summed_field_having_condition c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit) assert_nil c[1] @@ -228,7 +242,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition_from_select - c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit) + skip unless current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("min_credit_limit > 50").sum(:credit_limit) assert_nil c[1] assert_equal 60, c[2] assert_equal 53, c[9] @@ -414,10 +429,6 @@ class CalculationsTest < ActiveRecord::TestCase def test_count_with_distinct assert_equal 4, Account.select(:credit_limit).distinct.count - - assert_deprecated do - assert_equal 4, Account.select(:credit_limit).uniq.count - end end def test_count_with_aliased_attribute @@ -446,7 +457,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_field_in_joined_table_with_group_by c = Account.group("accounts.firm_id").joins(:firm).count("companies.id") - [1,6,2,9].each { |firm_id| assert_includes c.keys, firm_id } + [1, 6, 2, 9].each { |firm_id| assert_includes c.keys, firm_id } end def test_should_count_field_of_root_table_with_conflicting_group_by_column @@ -565,7 +576,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck - assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id) + assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck(:id) end def test_pluck_without_column_names @@ -601,7 +612,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_with_qualified_column_name - assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id") + assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck("topics.id") end def test_pluck_auto_table_name_prefix @@ -681,7 +692,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_replaces_select_clause taks_relation = Topic.select(:approved, :id).order(:id) - assert_equal [1,2,3,4,5], taks_relation.pluck(:id) + assert_equal [1, 2, 3, 4, 5], taks_relation.pluck(:id) assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end @@ -791,4 +802,16 @@ class CalculationsTest < ActiveRecord::TestCase def test_group_by_attribute_with_custom_type assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end + + def test_deprecate_count_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.count(:firm_id) { true } + end + end + + def test_deprecate_sum_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.sum(:firm_id) { 1 } + end + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 4b517e9d70..b3c86586d0 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -6,10 +6,6 @@ class CallbackDeveloper < ActiveRecord::Base self.table_name = "developers" class << self - def callback_string(callback_method) - "history << [#{callback_method.to_sym.inspect}, :string]" - end - def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end @@ -33,7 +29,6 @@ class CallbackDeveloper < ActiveRecord::Base ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s.start_with?("around_") define_callback_method(callback_method) - ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } @@ -44,11 +39,6 @@ class CallbackDeveloper < ActiveRecord::Base end end -class CallbackDeveloperWithFalseValidation < CallbackDeveloper - before_validation proc { |model| model.history << [:before_validation, :returning_false]; false } - before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } -end - class CallbackDeveloperWithHaltedValidation < CallbackDeveloper before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } @@ -125,11 +115,11 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base after_validation :after_validation_on_create_and_update, on: [ :create, :update ] def before_validation_on_create_and_update - history << "before_validation_on_#{self.validation_context}".to_sym + history << "before_validation_on_#{validation_context}".to_sym end def after_validation_on_create_and_update - history << "after_validation_on_#{self.validation_context}".to_sym + history << "after_validation_on_#{validation_context}".to_sym end def history @@ -137,23 +127,6 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base end end -class CallbackCancellationDeveloper < ActiveRecord::Base - self.table_name = "developers" - - attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called - attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy - - before_save { defined?(@cancel_before_save) ? !@cancel_before_save : false } - before_create { !@cancel_before_create } - before_update { !@cancel_before_update } - before_destroy { !@cancel_before_destroy } - - after_save { @after_save_called = true } - after_update { @after_update_called = true } - after_create { @after_create_called = true } - after_destroy { @after_destroy_called = true } -end - class CallbackHaltedDeveloper < ActiveRecord::Base self.table_name = "developers" @@ -178,7 +151,6 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.new assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], @@ -189,12 +161,10 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.find(1) assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], @@ -206,17 +176,14 @@ class CallbacksTest < ActiveRecord::TestCase david.valid? assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], @@ -228,22 +195,18 @@ class CallbacksTest < ActiveRecord::TestCase david.valid? assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], @@ -254,44 +217,36 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.create("name" => "David", "salary" => 1000000) assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], - [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_create, :method ], - [ :before_create, :string ], [ :before_create, :proc ], [ :before_create, :object ], [ :before_create, :block ], [ :after_create, :method ], - [ :after_create, :string ], [ :after_create, :proc ], [ :after_create, :object ], [ :after_create, :block ], [ :after_save, :method ], - [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -323,49 +278,40 @@ class CallbacksTest < ActiveRecord::TestCase david.save assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], - [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_update, :method ], - [ :before_update, :string ], [ :before_update, :proc ], [ :before_update, :object ], [ :before_update, :block ], [ :after_update, :method ], - [ :after_update, :string ], [ :after_update, :proc ], [ :after_update, :object ], [ :after_update, :block ], [ :after_save, :method ], - [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -399,29 +345,24 @@ class CallbacksTest < ActiveRecord::TestCase david.destroy assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_destroy, :method ], - [ :before_destroy, :string ], [ :before_destroy, :proc ], [ :before_destroy, :object ], [ :before_destroy, :block ], [ :after_destroy, :method ], - [ :after_destroy, :string ], [ :after_destroy, :proc ], [ :after_destroy, :object ], [ :after_destroy, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -431,82 +372,16 @@ class CallbacksTest < ActiveRecord::TestCase CallbackDeveloper.delete(david.id) assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], ], david.history end - def test_deprecated_before_save_returning_false - david = ImmutableDeveloper.find(1) - assert_deprecated do - assert david.valid? - assert !david.save - exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } - assert_equal exc.record, david - assert_equal "Failed to save the record", exc.message - end - - david = ImmutableDeveloper.find(1) - david.salary = 10_000_000 - assert !david.valid? - assert !david.save - assert_raise(ActiveRecord::RecordInvalid) { david.save! } - - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_save = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_create_returning_false - someone = CallbackCancellationDeveloper.new - someone.cancel_before_create = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_update_returning_false - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_update = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_destroy_returning_false - david = ImmutableDeveloper.find(1) - assert_deprecated do - assert !david.destroy - exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } - assert_equal exc.record, david - assert_equal "Failed to destroy the record", exc.message - end - assert_not_nil ImmutableDeveloper.find_by_id(1) - - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_destroy = true - assert_deprecated do - assert !someone.destroy - assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } - end - assert !someone.after_destroy_called - end - def assert_save_callbacks_not_called(someone) assert !someone.after_save_called assert !someone.after_create_called @@ -527,7 +402,7 @@ class CallbacksTest < ActiveRecord::TestCase assert david.valid? assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } - assert_equal exc.record, david + assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 @@ -554,7 +429,7 @@ class CallbacksTest < ActiveRecord::TestCase david = DeveloperWithCanceledCallbacks.find(1) assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } - assert_equal exc.record, david + assert_equal david, exc.record assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackHaltedDeveloper.find(1) @@ -564,50 +439,19 @@ class CallbacksTest < ActiveRecord::TestCase assert !someone.after_destroy_called end - def test_callback_returning_false - david = CallbackDeveloperWithFalseValidation.find(1) - assert_deprecated { david.save } - assert_equal [ - [ :after_find, :method ], - [ :after_find, :string ], - [ :after_find, :proc ], - [ :after_find, :object ], - [ :after_find, :block ], - [ :after_initialize, :method ], - [ :after_initialize, :string ], - [ :after_initialize, :proc ], - [ :after_initialize, :object ], - [ :after_initialize, :block ], - [ :before_validation, :method ], - [ :before_validation, :string ], - [ :before_validation, :proc ], - [ :before_validation, :object ], - [ :before_validation, :block ], - [ :before_validation, :returning_false ], - [ :after_rollback, :block ], - [ :after_rollback, :object ], - [ :after_rollback, :proc ], - [ :after_rollback, :string ], - [ :after_rollback, :method ], - ], david.history - end - def test_callback_throwing_abort david = CallbackDeveloperWithHaltedValidation.find(1) david.save assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], @@ -615,7 +459,6 @@ class CallbacksTest < ActiveRecord::TestCase [ :after_rollback, :block ], [ :after_rollback, :object ], [ :after_rollback, :proc ], - [ :after_rollback, :string ], [ :after_rollback, :method ], ], david.history end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index b9c6224425..a26a72712d 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -1,50 +1,51 @@ - require "cases/helper" module ActiveRecord module Coders class YAMLColumnTest < ActiveRecord::TestCase def test_initialize_takes_class - coder = YAMLColumn.new(Object) + coder = YAMLColumn.new("attr_name", Object) assert_equal Object, coder.object_class end def test_type_mismatch_on_different_classes_on_dump - coder = YAMLColumn.new(Array) - assert_raises(SerializationTypeMismatch) do + coder = YAMLColumn.new("tags", Array) + error = assert_raises(SerializationTypeMismatch) do coder.dump("a") end + assert_equal %{can't dump `tags`: was supposed to be a Array, but was a String. -- "a"}, error.to_s end def test_type_mismatch_on_different_classes - coder = YAMLColumn.new(Array) - assert_raises(SerializationTypeMismatch) do + coder = YAMLColumn.new("tags", Array) + error = assert_raises(SerializationTypeMismatch) do coder.load "--- foo" end + assert_equal %{can't load `tags`: was supposed to be a Array, but was a String. -- "foo"}, error.to_s end def test_nil_is_ok - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_nil coder.load "--- " end def test_returns_new_with_different_class - coder = YAMLColumn.new SerializationTypeMismatch + coder = YAMLColumn.new("attr_name", SerializationTypeMismatch) assert_equal SerializationTypeMismatch, coder.load("--- ").class end def test_returns_string_unless_starts_with_dash - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_equal "foo", coder.load("foo") end def test_load_handles_other_classes - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_equal [], coder.load([]) end def test_load_doesnt_swallow_yaml_exceptions - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") bad_yaml = "--- {" assert_raises(Psych::SyntaxError) do coder.load(bad_yaml) @@ -52,7 +53,7 @@ module ActiveRecord end def test_load_doesnt_handle_undefined_class_or_module - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n' assert_raises(ArgumentError) do coder.load(missing_class_yaml) diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index a2874438c1..f344c77691 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -11,16 +11,42 @@ module ActiveRecord fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts test "collection_cache_key on model" do - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, Developer.collection_cache_key) end test "cache_key for relation" do - developers = Developer.where(name: "David") - last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at + developers = Developer.where(salary: 100000).order(updated_at: :desc) + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key + + assert_equal Digest::MD5.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 + + test "cache_key for relation with limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + last_developer_timestamp = developers.first.updated_at - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) - /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key + + assert_equal Digest::MD5.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 + + test "cache_key for loaded relation" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 @@ -28,27 +54,27 @@ module ActiveRecord end test "it triggers at most one query" do - developers = Developer.where(name: "David") + developers = Developer.where(name: "David") assert_queries(1) { developers.cache_key } assert_queries(0) { developers.cache_key } end test "it doesn't trigger any query if the relation is already loaded" do - developers = Developer.where(name: "David").load + developers = Developer.where(name: "David").load assert_queries(0) { developers.cache_key } end test "relation cache_key changes when the sql query changes" do developers = Developer.where(name: "David") - other_relation = Developer.where(name: "David").where("1 = 1") + other_relation = Developer.where(name: "David").where("1 = 1") assert_not_equal developers.cache_key, other_relation.cache_key end test "cache_key for empty relation" do developers = Developer.where(name: "Non Existent Developer") - assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key) end test "cache_key with custom timestamp column" do @@ -64,7 +90,7 @@ module ActiveRecord test "collection proxy provides a cache_key" do developers = projects(:active_record).developers - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end test "cache_key for loaded collection with zero size" do @@ -72,18 +98,18 @@ module ActiveRecord posts = Post.includes(:comments) empty_loaded_collection = posts.first.comments - assert_match(/\Acomments\/query-(\h+)-0\Z/, empty_loaded_collection.cache_key) + assert_match(/\Acomments\/query-(\h+)-0\z/, empty_loaded_collection.cache_key) end test "cache_key for queries with offset which return 0 rows" do developers = Developer.offset(20) - assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key) end test "cache_key with a relation having selected columns" do developers = Developer.select(:salary) - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index a65bb89052..90c8d21c43 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -8,77 +8,25 @@ module ActiveRecord def @adapter.native_database_types { string: "varchar" } end - @viz = @adapter.schema_creation + @viz = @adapter.send(:schema_creation) end # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20)) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20) assert_equal "title varchar(20)", @viz.accept(column_def) end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20)) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello") assert_equal "title varchar(20) DEFAULT 'Hello'", @viz.accept(column_def) end def test_should_specify_not_null_if_null_option_is_false - type_metadata = SqlTypeMetadata.new(limit: 20) - column = Column.new("title", "Hello", type_metadata, false) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello", null: false) assert_equal "title varchar(20) DEFAULT 'Hello' NOT NULL", @viz.accept(column_def) end - - if current_adapter?(:Mysql2Adapter) - def test_should_set_default_for_mysql_binary_data_types - type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = MySQL::Column.new("title", "a", type) - assert_equal "a", binary_column.default - - type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = MySQL::Column.new("title", "a", type) - assert_equal "a", varbinary_column.default - end - - def test_should_be_empty_string_default_for_mysql_binary_data_types - type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = MySQL::Column.new("title", "", type, false) - assert_equal "", binary_column.default - - type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = MySQL::Column.new("title", "", type, false) - assert_equal "", varbinary_column.default - end - - def test_should_not_set_default_for_blob_and_text_data_types - text_type = MySQL::TypeMetadata.new(SqlTypeMetadata.new(type: :text)) - - text_column = MySQL::Column.new("title", nil, text_type) - assert_nil text_column.default - - not_null_text_column = MySQL::Column.new("title", nil, text_type, false) - assert_nil not_null_text_column.default - end - - def test_has_default_should_return_false_for_blob_and_text_data_types - binary_type = SqlTypeMetadata.new(sql_type: "blob") - blob_column = MySQL::Column.new("title", nil, binary_type) - assert !blob_column.has_default? - - text_type = SqlTypeMetadata.new(type: :text) - text_column = MySQL::Column.new("title", nil, text_type) - assert !text_column.has_default? - end - end end end end diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 262ad319be..c23be52a6c 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -2,7 +2,6 @@ require "cases/helper" require "support/schema_dumping_helper" if ActiveRecord::Base.connection.supports_comments? - class CommentTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -73,9 +72,11 @@ if ActiveRecord::Base.connection.supports_comments? end def test_add_index_with_comment_later - @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" - index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" } - assert_equal "We need to see obvious comments", index.comment + unless current_adapter?(:OracleAdapter) + @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" + index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" } + assert_equal "We need to see obvious comments", index.comment + end end def test_add_comment_to_column @@ -102,6 +103,7 @@ if ActiveRecord::Base.connection.supports_comments? # Do all the stuff from other tests @connection.add_column :commenteds, :rating, :integer, comment: "I am running out of imagination" @connection.change_column :commenteds, :content, :string, comment: "Whoa, content describes itself!" + @connection.change_column :commenteds, :content, :string @connection.change_column :commenteds, :obvious, :string, comment: nil @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" @@ -112,8 +114,10 @@ if ActiveRecord::Base.connection.supports_comments? assert_match %r[t\.string\s+"obvious"\n], output assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output - assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output - assert_match %r[t\.index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output + unless current_adapter?(:OracleAdapter) + assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output + assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output + end end def test_schema_dump_omits_blank_comments @@ -135,5 +139,4 @@ if ActiveRecord::Base.connection.supports_comments? assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output end end - end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index d5d16e7568..2a71f08d90 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -6,7 +6,18 @@ module ActiveRecord def setup @handler = ConnectionHandler.new @spec_name = "primary" - @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) + @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + def test_default_env_fall_back_to_default_env_when_rails_env_or_rack_env_is_empty_string + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "" + + assert_equal "default_env", ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + ensure + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env end def test_establish_connection_uses_spec_name @@ -20,6 +31,66 @@ module ActiveRecord @handler.remove_connection("readonly") end + def test_establish_connection_using_3_levels_config + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + }, + "another_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/bad-readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" } + }, + "common" => { "adapter" => "sqlite3", "database" => "db/common.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:common) + @handler.establish_connection(:primary) + @handler.establish_connection(:readonly) + + assert_not_nil pool = @handler.retrieve_connection_pool("readonly") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = @handler.retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = @handler.retrieve_connection_pool("common") + assert_equal "db/common.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + end + + def test_establish_connection_using_two_level_configurations + config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:development) + + assert_not_nil pool = @handler.retrieve_connection_pool("development") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + end + + def test_establish_connection_using_top_level_key_in_two_level_config + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:development_readonly) + + assert_not_nil pool = @handler.retrieve_connection_pool("development_readonly") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + end + def test_retrieve_connection assert @handler.retrieve_connection(@spec_name) end @@ -89,6 +160,41 @@ module ActiveRecord rd.close end + def test_pool_from_any_process_for_uses_most_recent_spec + skip unless current_adapter?(:SQLite3Adapter) + + file = Tempfile.new "lol.sqlite3" + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork do + ActiveRecord::Base.configurations["arunit"]["database"] = file.path + ActiveRecord::Base.establish_connection(:arunit) + + pid2 = fork do + wr.write ActiveRecord::Base.connection_config[:database] + wr.close + end + + Process.waitpid pid2 + end + + Process.waitpid pid + + wr.close + + assert_equal file.path, rd.read + + rd.close + ensure + if file + file.close + file.unlink + end + end + def test_a_class_using_custom_pool_and_switching_back_to_primary klass2 = Class.new(Base) { def self.name; "klass2"; end } diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 4bb5c4f2e2..8faa67255d 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -27,7 +27,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:default_env, config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost", "name"=>"default_env" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "default_env" } assert_equal expected, actual end @@ -37,7 +37,7 @@ module ActiveRecord config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:foo, config) - expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost","name"=>"foo" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "foo" } assert_equal expected, actual end @@ -47,7 +47,7 @@ module ActiveRecord config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:foo, config) - expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost","name"=>"foo" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "foo" } assert_equal expected, actual end @@ -55,13 +55,13 @@ module ActiveRecord ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } actual = resolve_spec(:production, config) - expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost", "name"=>"production" } + expected = { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost", "name" => "production" } assert_equal expected, actual end def test_resolver_with_database_uri_and_unknown_symbol_key ENV["DATABASE_URL"] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } assert_raises AdapterNotSpecified do resolve_spec(:production, config) end @@ -71,7 +71,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "not-postgres://not-localhost/not_foo" config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } actual = resolve_spec("postgres://localhost/foo", config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } assert_equal expected, actual end @@ -85,7 +85,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_config(config) - expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + expect_prod = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } assert_equal expect_prod, actual["default_env"] end @@ -93,7 +93,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "ibm-db://localhost/foo" config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } actual = resolve_spec(:default_env, config) - expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost", "name"=>"default_env" } + expected = { "adapter" => "ibm_db", "database" => "foo", "host" => "localhost", "name" => "default_env" } assert_equal expected, actual end @@ -142,13 +142,13 @@ module ActiveRecord "database" => "foo", "host" => "localhost" } assert_equal expected, actual["default_env"] - assert_equal nil, actual["production"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:default_env] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] + assert_nil actual["production"] + assert_nil actual["development"] + assert_nil actual["test"] + assert_nil actual[:default_env] + assert_nil actual[:production] + assert_nil actual[:development] + assert_nil actual[:test] end def test_blank_with_database_url_with_rails_env @@ -162,15 +162,15 @@ module ActiveRecord "host" => "localhost" } assert_equal expected, actual["not_production"] - assert_equal nil, actual["production"] - assert_equal nil, actual["default_env"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:default_env] - assert_equal nil, actual[:not_production] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] + assert_nil actual["production"] + assert_nil actual["default_env"] + assert_nil actual["development"] + assert_nil actual["test"] + assert_nil actual[:default_env] + assert_nil actual[:not_production] + assert_nil actual[:production] + assert_nil actual[:development] + assert_nil actual[:test] end def test_blank_with_database_url_with_rack_env @@ -184,15 +184,15 @@ module ActiveRecord "host" => "localhost" } assert_equal expected, actual["not_production"] - assert_equal nil, actual["production"] - assert_equal nil, actual["default_env"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:default_env] - assert_equal nil, actual[:not_production] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] + assert_nil actual["production"] + assert_nil actual["default_env"] + assert_nil actual["development"] + assert_nil actual["test"] + assert_nil actual[:default_env] + assert_nil actual[:not_production] + assert_nil actual[:production] + assert_nil actual[:development] + assert_nil actual[:test] end def test_database_url_with_ipv6_host_and_port @@ -213,7 +213,7 @@ module ActiveRecord config = { "default_env" => { "url" => "postgres://localhost/foo" } } actual = resolve_config(config) expected = { "default_env" => - { "adapter" => "postgresql", + { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } diff --git a/activerecord/test/cases/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb deleted file mode 100644 index 59dcb96ebc..0000000000 --- a/activerecord/test/cases/connection_adapters/quoting_test.rb +++ /dev/null @@ -1,13 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - module Quoting - class QuotingTest < ActiveRecord::TestCase - def test_quoting_classes - assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object) - end - end - 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 d4459603af..106323ccc9 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -12,6 +12,33 @@ module ActiveRecord assert_equal "id", @cache.primary_keys("posts") end + def test_yaml_dump_and_load + @cache.columns("posts") + @cache.columns_hash("posts") + @cache.data_sources("posts") + @cache.primary_keys("posts") + + 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 new_cache.data_sources("posts") + assert_equal "id", new_cache.primary_keys("posts") + end + end + + def test_yaml_loads_5_1_dump + body = File.open(schema_dump_path).read + cache = YAML.load(body) + + assert_no_queries do + assert_equal 11, cache.columns("posts").size + assert_equal 11, cache.columns_hash("posts").size + assert cache.data_sources("posts") + assert_equal "id", cache.primary_keys("posts") + end + end + def test_primary_key_for_non_existent_table assert_nil @cache.primary_keys("omgponies") end @@ -45,17 +72,28 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 11, @cache.columns("posts").size - assert_equal 11, @cache.columns_hash("posts").size - assert @cache.data_sources("posts") - assert_equal "id", @cache.primary_keys("posts") + assert_no_queries do + assert_equal 11, @cache.columns("posts").size + assert_equal 11, @cache.columns_hash("posts").size + assert @cache.data_sources("posts") + assert_equal "id", @cache.primary_keys("posts") + end end - def test_table_methods_deprecation - assert_deprecated { assert @cache.table_exists?("posts") } - assert_deprecated { assert @cache.tables("posts") } - assert_deprecated { @cache.clear_table_cache!("posts") } + def test_data_source_exist + assert @cache.data_source_exists?("posts") + assert_not @cache.data_source_exists?("foo") end + + def test_clear_data_source_cache + @cache.clear_data_source_cache!("posts") + end + + private + + def schema_dump_path + "test/assets/schema_dump_5_1.yml" + end end end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index e2e5445a4e..a348c2d783 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -89,12 +89,20 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_decimal_without_scale - types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} - types.each do |type| - cast_type = @connection.type_map.lookup(type) - - assert_equal :decimal, cast_type.type - assert_equal 2, cast_type.cast(2.1) + if current_adapter?(:OracleAdapter) + { + decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0)}, + integer: %w{number(2) number(2,0)} + } + else + { decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} } + end.each do |expected_type, types| + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal expected_type, cast_type.type + assert_equal 2, cast_type.cast(2.1) + end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index d7ff9d6880..7e88c9cf7a 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -184,14 +184,14 @@ module ActiveRecord def test_checkout_behaviour pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec - connection = pool.connection - assert_not_nil connection + main_connection = pool.connection + assert_not_nil main_connection threads = [] 4.times do |i| threads << Thread.new(i) do - connection = pool.connection - assert_not_nil connection - connection.close + thread_connection = pool.connection + assert_not_nil thread_connection + thread_connection.close end end @@ -307,14 +307,17 @@ module ActiveRecord end end - def test_automatic_reconnect= + def test_automatic_reconnect_restores_after_disconnect pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec assert pool.automatic_reconnect assert pool.connection pool.disconnect! assert pool.connection + end + def test_automatic_reconnect_can_be_disabled + pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec pool.disconnect! pool.automatic_reconnect = false @@ -341,14 +344,18 @@ module ActiveRecord end end + class ConnectionTestModel < ActiveRecord::Base + end + def test_connection_notification_is_called payloads = [] subscription = ActiveSupport::Notifications.subscribe("!connection.active_record") do |name, started, finished, unique_id, payload| payloads << payload end - ActiveRecord::Base.establish_connection :arunit + ConnectionTestModel.establish_connection :arunit + assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort - assert_equal "primary", payloads[0][:spec_name] + assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name] ensure ActiveSupport::Notifications.unsubscribe(subscription) if subscription end @@ -395,7 +402,7 @@ module ActiveRecord all_threads_in_new_connection.wait end rescue Timeout::Error - flunk "pool unable to establish connections concurrently or implementation has " << + flunk "pool unable to establish connections concurrently or implementation has " \ "changed, this test then needs to patch a different :new_connection method" ensure # clean up the threads @@ -437,7 +444,7 @@ module ActiveRecord end end - def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_all_connections_proceed_anyway @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| @@ -455,49 +462,63 @@ module ActiveRecord with_single_connection_pool do |pool| [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| conn = pool.connection # drain the only available connection - second_thread_done = Concurrent::CountDownLatch.new - - # create a first_thread and let it get into the FIFO queue first - first_thread = Thread.new do - pool.with_connection { second_thread_done.wait } - end - - # wait for first_thread to get in queue - Thread.pass until pool.num_waiting_in_queue == 1 - - # create a different, later thread, that will attempt to do a "group action", - # but because of the group action semantics it should be able to preempt the - # first_thread when a connection is made available - second_thread = Thread.new do - pool.send(group_action_method) - second_thread_done.count_down - end - - # wait for second_thread to get in queue - Thread.pass until pool.num_waiting_in_queue == 2 - - # return the only available connection - pool.checkin(conn) + second_thread_done = Concurrent::Event.new - # if the second_thread is not able to preempt the first_thread, - # they will temporarily (until either of them timeouts with ConnectionTimeoutError) - # deadlock and a join(2) timeout will be reached - failed = true unless second_thread.join(2) - - #--- post test clean up start - second_thread_done.count_down if failed - - # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for - # it to timeout with ConnectionTimeoutError - if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0 - pool.with_connection {} # create a new connection in case there are threads still stuck in a queue + begin + # create a first_thread and let it get into the FIFO queue first + first_thread = Thread.new do + pool.with_connection { second_thread_done.wait } + end + + # wait for first_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + # create a different, later thread, that will attempt to do a "group action", + # but because of the group action semantics it should be able to preempt the + # first_thread when a connection is made available + second_thread = Thread.new do + pool.send(group_action_method) + second_thread_done.set + end + + # wait for second_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 2 + + # return the only available connection + pool.checkin(conn) + + # if the second_thread is not able to preempt the first_thread, + # they will temporarily (until either of them timeouts with ConnectionTimeoutError) + # deadlock and a join(2) timeout will be reached + assert second_thread.join(2), "#{group_action_method} is not able to preempt other waiting threads" + + ensure + # post test clean up + failed = !second_thread_done.set? + + if failed + second_thread_done.set + + puts + puts ">>> test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads / #{group_action_method}" + p [first_thread, second_thread] + p pool.stat + p pool.connections.map(&:owner) + + first_thread.join(2) + second_thread.join(2) + + puts "---" + p [first_thread, second_thread] + p pool.stat + p pool.connections.map(&:owner) + puts "<<<" + puts + end + + first_thread.join(10) || raise("first_thread got stuck") + second_thread.join(10) || raise("second_thread got stuck") end - - first_thread.join - second_thread.join - #--- post test clean up end - - flunk "#{group_action_method} is not able to preempt other waiting threads" if failed end end end @@ -526,6 +547,26 @@ module ActiveRecord end end + def test_connection_pool_stat + with_single_connection_pool do |pool| + pool.with_connection do |connection| + stats = pool.stat + assert_equal({ size: 1, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }, stats) + end + + stats = pool.stat + assert_equal({ size: 1, connections: 1, busy: 0, dead: 0, idle: 1, waiting: 0, checkout_timeout: 5 }, stats) + + Thread.new do + pool.checkout + Thread.current.kill + end.join + + stats = pool.stat + assert_equal({ size: 1, connections: 1, busy: 0, dead: 1, idle: 0, waiting: 0, checkout_timeout: 5 }, stats) + end + end + private def with_single_connection_pool one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 0f62c73f8f..13b5bae13c 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -4,11 +4,11 @@ module ActiveRecord module ConnectionAdapters class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase - def resolve(spec, config={}) + def resolve(spec, config = {}) Resolver.new(config).resolve(spec) end - def spec(spec, config={}) + def spec(spec, config = {}) Resolver.new(config).spec(spec) end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 84f2c3a465..46d7526cc0 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -211,4 +211,155 @@ class CounterCacheTest < ActiveRecord::TestCase aircraft.wheels.first.destroy end end + + test "update counters doesn't touch timestamps by default" do + @topic.update_column :updated_at, 5.minutes.ago + previously_updated_at = @topic.updated_at + + Topic.update_counters(@topic.id, replies_count: -1) + + assert_equal previously_updated_at, @topic.updated_at + end + + test "update counters doesn't touch timestamps with touch: []" do + @topic.update_column :updated_at, 5.minutes.ago + previously_updated_at = @topic.updated_at + + Topic.update_counters(@topic.id, replies_count: -1, touch: []) + + assert_equal previously_updated_at, @topic.updated_at + end + + test "update counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.update_counters(@topic.id, replies_count: -1, touch: true) + end + end + + test "update counters of multiple records with touch: true" do + t1, t2 = topics(:first, :second) + + assert_touching t1, :updated_at do + assert_difference ["t1.reload.replies_count", "t2.reload.replies_count"], 2 do + Topic.update_counters([t1.id, t2.id], replies_count: 2, touch: true) + end + end + end + + test "update multiple counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: true) + end + end + + test "reset counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.reset_counters(@topic.id, :replies, touch: true) + end + end + + test "reset multiple counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) + Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: true) + end + end + + test "increment counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.increment_counter(:replies_count, @topic.id, touch: true) + end + end + + test "decrement counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.decrement_counter(:replies_count, @topic.id, touch: true) + end + end + + test "update counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.update_counters(@topic.id, replies_count: -1, touch: :written_on) + end + end + + test "update multiple counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: :written_on) + end + end + + test "reset counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.reset_counters(@topic.id, :replies, touch: :written_on) + end + end + + test "reset multiple counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) + Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: :written_on) + end + end + + test "increment counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.increment_counter(:replies_count, @topic.id, touch: :written_on) + end + end + + test "decrement counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.decrement_counter(:replies_count, @topic.id, touch: :written_on) + end + end + + test "update counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.update_counters(@topic.id, replies_count: -1, touch: %i( updated_at written_on )) + end + end + + test "update multiple counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: %i( updated_at written_on )) + end + end + + test "reset counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.reset_counters(@topic.id, :replies, touch: %i( updated_at written_on )) + end + end + + test "reset multiple counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) + Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: %i( updated_at written_on )) + end + end + + test "increment counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.increment_counter(:replies_count, @topic.id, touch: %i( updated_at written_on )) + end + end + + test "decrement counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.decrement_counter(:replies_count, @topic.id, touch: %i( updated_at written_on )) + end + end + + private + def assert_touching(record, *attributes) + record.update_columns attributes.map { |attr| [ attr, 5.minutes.ago ] }.to_h + touch_times = attributes.map { |attr| [ attr, record.public_send(attr) ] }.to_h + + yield + + touch_times.each do |attr, previous_touch_time| + assert_operator previous_touch_time, :<, record.reload.public_send(attr) + end + end end diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index bb16076fd2..66035865be 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -20,12 +20,6 @@ class DatabaseStatementsTest < ActiveRecord::TestCase assert_not_nil return_the_inserted_id(method: :create) end - def test_insert_update_delete_sql_is_deprecated - assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") } - assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") } - assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") } - end - private def return_the_inserted_id(method:) diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index a1c3c5af9c..e4a2f9ee17 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -73,7 +73,7 @@ if subsecond_precision_supported? assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_datetime_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 0 diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 3bc08f80ec..ad7da9de70 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -52,7 +52,7 @@ class DateTimeTest < ActiveRecord::TestCase end def test_assign_in_local_timezone - now = DateTime.now + now = DateTime.civil(2017, 3, 1, 12, 0, 0) with_timezone_config default: :local do task = Task.new starting: now assert_equal now, task.starting diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index fcaff38f82..996d298689 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -87,9 +87,14 @@ if current_adapter?(:PostgreSQLAdapter) test "schema dump includes default expression" do output = dump_table_schema("defaults") - assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + if ActiveRecord::Base.connection.postgresql_version >= 100000 + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + else + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output + end assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output - assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output end end @@ -100,11 +105,21 @@ if current_adapter?(:Mysql2Adapter) include SchemaDumpingHelper if ActiveRecord::Base.connection.version >= "5.6.0" - test "schema dump includes default expression" do + test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output end end + + test "schema dump timestamp includes default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + end + + test "schema dump timestamp without default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + end end class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase @@ -169,7 +184,7 @@ if current_adapter?(:Mysql2Adapter) assert_nil record.non_null_text assert_nil record.non_null_blob - assert_raises(ActiveRecord::StatementInvalid) { klass.create } + assert_raises(ActiveRecord::NotNullViolation) { klass.create } end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index eee34da664..f72e0d2ead 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -298,6 +298,14 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["arr", "arr matey!"], pirate.catchphrase_change end + def test_virtual_attribute_will_change + assert_deprecated do + parrot = Parrot.create!(name: "Ruby") + parrot.send(:attribute_will_change!, :cancel_save_from_callback) + assert parrot.has_changes_to_save? + end + end + def test_association_assignment_changes_foreign_key pirate = Pirate.create!(catchphrase: "jarl") pirate.parrot = Parrot.create!(name: "Lorre") @@ -338,13 +346,14 @@ class DirtyTest < ActiveRecord::TestCase def test_partial_update_with_optimistic_locking person = Person.new(first_name: "foo") - old_lock_version = 1 with_partial_writes Person, false do assert_queries(2) { 2.times { person.save! } } Person.where(id: person.id).update_all(first_name: "baz") end + old_lock_version = person.lock_version + with_partial_writes Person, true do assert_queries(0) { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version @@ -555,18 +564,17 @@ class DirtyTest < ActiveRecord::TestCase travel_back end - if ActiveRecord::Base.connection.supports_migrations? - class Testings < ActiveRecord::Base; end - def test_field_named_field - ActiveRecord::Base.connection.create_table :testings do |t| - t.string :field - end - assert_nothing_raised do - Testings.new.attributes - end - ensure - ActiveRecord::Base.connection.drop_table :testings rescue nil + class Testings < ActiveRecord::Base; end + def test_field_named_field + ActiveRecord::Base.connection.create_table :testings do |t| + t.string :field end + assert_nothing_raised do + Testings.new.attributes + end + ensure + ActiveRecord::Base.connection.drop_table :testings rescue nil + ActiveRecord::Base.clear_cache! end def test_datetime_attribute_can_be_updated_with_fractional_seconds @@ -660,6 +668,47 @@ class DirtyTest < ActiveRecord::TestCase assert binary.changed? end + test "changes is correct for subclass" do + foo = Class.new(Pirate) do + def catchphrase + super.upcase + end + end + + pirate = foo.create!(catchphrase: "arrrr") + + new_catchphrase = "arrrr matey!" + + pirate.catchphrase = new_catchphrase + assert pirate.catchphrase_changed? + + expected_changes = { + "catchphrase" => ["arrrr", new_catchphrase] + } + + assert_equal new_catchphrase.upcase, pirate.catchphrase + assert_equal expected_changes, pirate.changes + end + + test "changes is correct if override attribute reader" do + pirate = Pirate.create!(catchphrase: "arrrr") + def pirate.catchphrase + super.upcase + end + + new_catchphrase = "arrrr matey!" + + pirate.catchphrase = new_catchphrase + assert pirate.catchphrase_changed? + + expected_changes = { + "catchphrase" => ["arrrr", new_catchphrase] + } + + assert_equal new_catchphrase.upcase, pirate.catchphrase + assert_equal expected_changes, pirate.changes + end + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do test_type_class = Class.new(ActiveRecord::Type::Value) do define_method(:changed_in_place?) do |*| @@ -723,6 +772,89 @@ class DirtyTest < ActiveRecord::TestCase assert person.changed? 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 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") + end + + test "saved_change_to_attribute returns the change that occurred in the last save" do + person = Person.create!(first_name: "Sean", gender: "M") + + assert_equal [nil, "Sean"], person.saved_change_to_first_name + assert_equal [nil, "M"], person.saved_change_to_gender + + person.update(first_name: "Jim") + + assert_equal ["Sean", "Jim"], person.saved_change_to_first_name + assert_nil person.saved_change_to_gender + end + + test "attribute_before_last_save returns the original value before saving" do + person = Person.create!(first_name: "Sean", gender: "M") + + assert_nil person.first_name_before_last_save + assert_nil person.gender_before_last_save + + person.first_name = "Jim" + + assert_nil person.first_name_before_last_save + assert_nil person.gender_before_last_save + + person.save + + assert_equal "Sean", person.first_name_before_last_save + assert_equal "M", person.gender_before_last_save + end + + test "saved_changes? returns whether the last call to save changed anything" do + person = Person.create!(first_name: "Sean") + + assert person.saved_changes? + + person.save + + refute person.saved_changes? + end + + test "saved_changes returns a hash of all the changes that occurred" do + person = Person.create!(first_name: "Sean", gender: "M") + + assert_equal [nil, "Sean"], person.saved_changes[:first_name] + assert_equal [nil, "M"], person.saved_changes[:gender] + assert_equal %w(id first_name gender created_at updated_at).sort, person.saved_changes.keys.sort + + travel(1.second) do + person.update(first_name: "Jim") + end + + assert_equal ["Sean", "Jim"], person.saved_changes[:first_name] + assert_equal %w(first_name lock_version updated_at).sort, person.saved_changes.keys.sort + end + + test "changed? in after callbacks returns true but is deprecated" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "people" + + after_save do + ActiveSupport::Deprecation.silence do + raise "changed? should be true" unless changed? + end + raise "has_changes_to_save? should be false" if has_changes_to_save? + end + end + + person = klass.create!(first_name: "Sean") + refute person.changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 3821e0c949..000ed27efb 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -143,6 +143,8 @@ module ActiveRecord end def test_dup_without_primary_key + skip if current_adapter?(:OracleAdapter) + klass = Class.new(ActiveRecord::Base) do self.table_name = "parrots_pirates" end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index b7641fcf32..db3da53487 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -1,8 +1,9 @@ require "cases/helper" +require "models/author" require "models/book" class EnumTest < ActiveRecord::TestCase - fixtures :books + fixtures :books, :authors setup do @book = books(:awdr) @@ -37,6 +38,8 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.author_visibility_visible.first assert_equal @book, Book.illustrator_visibility_visible.first assert_equal @book, Book.medium_to_read.first + assert_equal books(:ddd), Book.forgotten.first + assert_equal books(:rfr), authors(:david).unpublished_books.first end test "find via where with values" do diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index 0711a372f2..73feb831d0 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -5,7 +5,7 @@ class ErrorsTest < ActiveRecord::TestCase base = ActiveRecord::ActiveRecordError error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base } - error_klasses.each do |error_klass| + (error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass| begin error_klass.new.inspect rescue ArgumentError diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 86fe90ae51..4f6bd9327c 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -47,7 +47,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_exec_explain_with_binds sqls = %w(foo bar) - binds = [[bind_param("wadus", 1)], [bind_param("chaflan", 2)]] + binds = [[bind_attribute("wadus", 1)], [bind_attribute("chaflan", 2)]] queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do @@ -79,9 +79,5 @@ if ActiveRecord::Base.connection.supports_explain? yield end end - - def bind_param(name, value) - ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) - end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 51563b347c..4837a169fa 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -49,22 +49,22 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_ids_returning_ordered - records = Topic.find([4,2,5]) + records = Topic.find([4, 2, 5]) assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.find(4,2,5) + records = Topic.find(4, 2, 5) assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.find(["4","2","5"]) + records = Topic.find(["4", "2", "5"]) assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.find("4","2","5") + records = Topic.find("4", "2", "5") assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title @@ -72,12 +72,12 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_and_order_clause # The order clause takes precedence over the informed ids - records = Topic.order(:author_name).find([5,3,1]) + records = Topic.order(:author_name).find([5, 3, 1]) assert_equal "The Third Topic of the day", records[0].title assert_equal "The First Topic", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.order(:id).find([5,3,1]) + records = Topic.order(:id).find([5, 3, 1]) assert_equal "The First Topic", records[0].title assert_equal "The Third Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title @@ -85,14 +85,14 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_with_limit_and_order_clause # The order clause takes precedence over the informed ids - records = Topic.limit(2).order(:id).find([5,3,1]) + records = Topic.limit(2).order(:id).find([5, 3, 1]) assert_equal 2, records.size assert_equal "The First Topic", records[0].title assert_equal "The Third Topic of the day", records[1].title end def test_find_with_ids_and_limit - records = Topic.limit(3).find([3,2,5,1,4]) + records = Topic.limit(3).find([3, 2, 5, 1, 4]) assert_equal 3, records.size assert_equal "The Third Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title @@ -102,7 +102,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_where_and_limit # Please note that Topic 1 is the only not approved so # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound - records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) + records = Topic.where(approved: true).limit(3).find([3, 2, 5, 1, 4]) assert_equal 3, records.size assert_equal "The Third Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title @@ -110,15 +110,15 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_ids_and_offset - records = Topic.offset(2).find([3,2,5,1,4]) + records = Topic.offset(2).find([3, 2, 5, 1, 4]) assert_equal 3, records.size assert_equal "The Fifth Topic of the day", records[0].title assert_equal "The First Topic", records[1].title assert_equal "The Fourth Topic of the day", records[2].title end - def test_find_passing_active_record_object_is_deprecated - assert_deprecated do + def test_find_passing_active_record_object_is_not_permitted + assert_raises(ArgumentError) do Topic.find(Topic.last) end end @@ -136,7 +136,7 @@ class FinderTest < ActiveRecord::TestCase # find should handle strings that come from URLs # (example: Category.find(params[:id])) def test_find_with_string - assert_equal(Topic.find(1).title,Topic.find("1").title) + assert_equal(Topic.find(1).title, Topic.find("1").title) end def test_exists @@ -151,7 +151,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, Topic.exists?(45) assert_equal false, Topic.exists?(Topic.new.id) - assert_raise(NoMethodError) { Topic.exists?([1,2]) } + assert_raise(NoMethodError) { Topic.exists?([1, 2]) } end def test_exists_with_polymorphic_relation @@ -167,15 +167,15 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, relation.exists?(false) end - def test_exists_passing_active_record_object_is_deprecated - assert_deprecated do + def test_exists_passing_active_record_object_is_not_permitted + assert_raises(ArgumentError) do Topic.exists?(Topic.new) end end def test_exists_returns_false_when_parameter_has_invalid_type assert_equal false, Topic.exists?("foo") - assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int + assert_equal false, Topic.exists?(("9" * 53).to_i) # number that's bigger than int end def test_exists_does_not_select_columns_without_alias @@ -202,11 +202,29 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.first.replies.exists? end - # ensures +exists?+ runs valid SQL by excluding order value - def test_exists_with_order + # Ensure +exists?+ runs without an error by excluding distinct value. + # See https://github.com/rails/rails/pull/26981. + def test_exists_with_order_and_distinct assert_equal true, Topic.order(:id).distinct.exists? end + # Ensure +exists?+ runs without an error by excluding order value. + def test_exists_with_order + assert_equal true, Topic.order("invalid sql here").exists? + end + + def test_exists_with_joins + assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + + def test_exists_with_left_joins + assert_equal true, Topic.left_joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + + def test_exists_with_eager_load + assert_equal true, Topic.eager_load(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + 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? @@ -236,9 +254,9 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_aggregate_having_three_mappings_with_one_difference existing_address = customers(:david).address - assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) - assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) - assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end def test_exists_does_not_instantiate_records @@ -258,8 +276,8 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.limit(2).find([1,3,2]).size - entrants = Entrant.limit(3).offset(2).find([1,3,2]) + assert_equal 2, Entrant.limit(2).find([1, 3, 2]).size + entrants = Entrant.limit(3).offset(2).find([1, 3, 2]) assert_equal 1, entrants.size assert_equal "Ruby Guru", entrants.first.name @@ -339,6 +357,11 @@ class FinderTest < ActiveRecord::TestCase assert_equal author.post, Post.find_by(author_id: Author.where(id: author)) end + def test_find_by_and_where_consistency_with_active_record_instance + author = authors(:david) + assert_equal Post.where(author_id: author).take, Post.find_by(author_id: author) + end + def test_take assert_equal topics(:first), Topic.take end @@ -488,12 +511,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal topics(:fourth), Topic.offset(1).second_to_last assert_equal topics(:fourth), Topic.offset(2).second_to_last assert_equal topics(:fourth), Topic.offset(3).second_to_last - assert_equal nil, Topic.offset(4).second_to_last - assert_equal nil, Topic.offset(5).second_to_last + assert_nil Topic.offset(4).second_to_last + assert_nil Topic.offset(5).second_to_last #test with limit - # assert_equal nil, Topic.limit(1).second # TODO: currently failing - assert_equal nil, Topic.limit(1).second_to_last + assert_nil Topic.limit(1).second + assert_nil Topic.limit(1).second_to_last end def test_second_to_last_have_primary_key_order_by_default @@ -516,15 +539,15 @@ class FinderTest < ActiveRecord::TestCase # test with offset assert_equal topics(:third), Topic.offset(1).third_to_last assert_equal topics(:third), Topic.offset(2).third_to_last - assert_equal nil, Topic.offset(3).third_to_last - assert_equal nil, Topic.offset(4).third_to_last - assert_equal nil, Topic.offset(5).third_to_last + assert_nil Topic.offset(3).third_to_last + assert_nil Topic.offset(4).third_to_last + assert_nil Topic.offset(5).third_to_last # test with limit - # assert_equal nil, Topic.limit(1).third # TODO: currently failing - assert_equal nil, Topic.limit(1).third_to_last - # assert_equal nil, Topic.limit(2).third # TODO: currently failing - assert_equal nil, Topic.limit(2).third_to_last + assert_nil Topic.limit(1).third + assert_nil Topic.limit(1).third_to_last + assert_nil Topic.limit(2).third + assert_nil Topic.limit(2).third_to_last end def test_third_to_last_have_primary_key_order_by_default @@ -584,7 +607,7 @@ class FinderTest < ActiveRecord::TestCase end def test_last_on_loaded_relation_should_not_use_sql - relation = Topic.limit(10).load + relation = Topic.limit(10).load assert_no_queries do relation.last relation.last(2) @@ -592,7 +615,7 @@ class FinderTest < ActiveRecord::TestCase end def test_last_with_irreversible_order - assert_deprecated do + assert_raises(ActiveRecord::IrreversibleOrderError) do Topic.order("coalesce(author_name, title)").last end end @@ -693,34 +716,33 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort + assert_equal [1, 2], Topic.where(id: 1..2).to_a.map(&:id).sort assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort - assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort + assert_equal [1, 2, 3], Topic.where(id: 1..3).to_a.map(&:id).sort + assert_equal [1, 2], Topic.where(id: 1...3).to_a.map(&:id).sort assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort + assert_equal [1, 2, 3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort + assert_equal [1, 2, 3, 5, 6, 7, 8, 9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_ranges - assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort + assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort end def test_find_on_multiple_hash_conditions assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } end def test_condition_interpolation @@ -858,13 +880,13 @@ class FinderTest < ActiveRecord::TestCase end def test_bind_variables_with_quotes - Company.create("name" => "37signals' go'es agains") - assert Company.where(["name = ?", "37signals' go'es agains"]).first + Company.create("name" => "37signals' go'es against") + assert Company.where(["name = ?", "37signals' go'es against"]).first end def test_named_bind_variables_with_quotes - Company.create("name" => "37signals' go'es agains") - assert Company.where(["name = :name", { name: "37signals' go'es agains" }]).first + Company.create("name" => "37signals' go'es against") + assert Company.where(["name = :name", { name: "37signals' go'es against" }]).first end def test_named_bind_variables @@ -983,7 +1005,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal devs[2], Developer.offset(2).first assert_equal devs[-3], Developer.offset(2).last - assert_equal devs[-3], Developer.offset(2).last assert_equal devs[-3], Developer.offset(2).order("id DESC").first end @@ -1029,7 +1050,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_id_with_conditions_with_or assert_nothing_raised do - Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1,2,3]) + Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1, 2, 3]) end end @@ -1061,8 +1082,8 @@ class FinderTest < ActiveRecord::TestCase end def test_select_values - assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s) - assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") + assert_equal ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s) + assert_equal ["37signals", "Summit", "Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") end def test_select_rows @@ -1167,12 +1188,12 @@ class FinderTest < ActiveRecord::TestCase end test "find_by returns nil if the record is missing" do - assert_equal nil, Post.find_by("1 = 0") + assert_nil Post.find_by("1 = 0") end 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 @@ -1221,7 +1242,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) end - protected + private def table_with_custom_primary_key yield(Class.new(Toy) do def self.name diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index cf2a73595a..533edcc2e0 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -31,7 +31,7 @@ module ActiveRecord def test_values File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) do |fh| - assert_equal [1,2,3,4,5,6].sort, fh.to_a.map(&:last).map { |x| + assert_equal [1, 2, 3, 4, 5, 6].sort, fh.to_a.map(&:last).map { |x| x["id"] }.sort end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 3f111447ff..a0a6d3c7ef 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -7,17 +7,19 @@ require "models/binary" require "models/book" require "models/bulb" require "models/category" +require "models/post" require "models/comment" require "models/company" require "models/computer" require "models/course" require "models/developer" +require "models/dog" require "models/doubloon" require "models/joke" require "models/matey" +require "models/other_dog" require "models/parrot" require "models/pirate" -require "models/post" require "models/randomly_named_c1" require "models/reply" require "models/ship" @@ -93,6 +95,24 @@ class FixturesTest < ActiveRecord::TestCase assert_nil(topics["second"]["author_email_address"]) end + def test_no_args_returns_all + all_topics = topics + assert_equal 5, all_topics.length + assert_equal "The First Topic", all_topics.first["title"] + assert_equal 5, all_topics.last.id + end + + def test_no_args_record_returns_all_without_array + all_binaries = binaries + assert_kind_of(Array, all_binaries) + assert_equal 1, binaries.length + end + + def test_nil_raises + assert_raise(StandardError) { topics(nil) } + assert_raise(StandardError) { topics([nil]) } + end + def test_inserts create_fixtures("topics") first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'") @@ -102,64 +122,62 @@ class FixturesTest < ActiveRecord::TestCase assert_nil(second_row["author_email_address"]) end - if ActiveRecord::Base.connection.supports_migrations? - def test_inserts_with_pre_and_suffix - # Reset cache to make finds on the new table work - ActiveRecord::FixtureSet.reset_cache - - ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| - t.column :title, :string - t.column :author_name, :string - t.column :author_email_address, :string - t.column :written_on, :datetime - t.column :bonus_time, :time - t.column :last_read, :date - t.column :content, :string - t.column :approved, :boolean, default: true - t.column :replies_count, :integer, default: 0 - t.column :parent_id, :integer - t.column :type, :string, limit: 50 - end + def test_inserts_with_pre_and_suffix + # Reset cache to make finds on the new table work + ActiveRecord::FixtureSet.reset_cache - # Store existing prefix/suffix - old_prefix = ActiveRecord::Base.table_name_prefix - old_suffix = ActiveRecord::Base.table_name_suffix + ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| + t.column :title, :string + t.column :author_name, :string + t.column :author_email_address, :string + t.column :written_on, :datetime + t.column :bonus_time, :time + t.column :last_read, :date + t.column :content, :string + t.column :approved, :boolean, default: true + t.column :replies_count, :integer, default: 0 + t.column :parent_id, :integer + t.column :type, :string, limit: 50 + end - # Set a prefix/suffix we can test against - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" + # Store existing prefix/suffix + old_prefix = ActiveRecord::Base.table_name_prefix + old_suffix = ActiveRecord::Base.table_name_suffix - other_topic_klass = Class.new(ActiveRecord::Base) do - def self.name - "OtherTopic" - end + # Set a prefix/suffix we can test against + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + + other_topic_klass = Class.new(ActiveRecord::Base) do + def self.name + "OtherTopic" end + end - topics = [create_fixtures("other_topics")].flatten.first + topics = [create_fixtures("other_topics")].flatten.first - # This checks for a caching problem which causes a bug in the fixtures - # class-level configuration helper. - assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" + # This checks for a caching problem which causes a bug in the fixtures + # class-level configuration helper. + assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" - first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") - assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" - assert_equal("The First Topic", first_row["title"]) + first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") + assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" + assert_equal("The First Topic", first_row["title"]) - second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") - assert_nil(second_row["author_email_address"]) + second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") + assert_nil(second_row["author_email_address"]) - assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym - # This assertion should preferably be the last in the list, because calling - # other_topic_klass.table_name sets a class-level instance variable - assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym + assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym + # This assertion should preferably be the last in the list, because calling + # other_topic_klass.table_name sets a class-level instance variable + assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym - ensure - # Restore prefix/suffix to its previous values - ActiveRecord::Base.table_name_prefix = old_prefix - ActiveRecord::Base.table_name_suffix = old_suffix + ensure + # Restore prefix/suffix to its previous values + ActiveRecord::Base.table_name_prefix = old_prefix + ActiveRecord::Base.table_name_suffix = old_suffix - ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil - end + ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil end def test_insert_with_datetime @@ -192,28 +210,38 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end 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? + assert Dir[nonexistent_fixture_path + "*"].empty? assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path) + ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file - assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses") + fixture_path = FIXTURES_ROOT + "/naked/yml/courses" + error = assert_raise(ActiveRecord::Fixture::FormatError) do + ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + end + assert_equal "fixture is not a hash: #{fixture_path}.yml", error.to_s + end + + def test_yaml_file_with_one_invalid_fixture + fixture_path = FIXTURES_ROOT + "/naked/yml/courses_with_invalid_key" + error = assert_raise(ActiveRecord::Fixture::FormatError) do + ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) end + assert_equal "fixture key is not a hash: #{fixture_path}.yml, keys: [\"two\"]", error.to_s end def test_yaml_file_with_invalid_column @@ -628,6 +656,8 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase def test_transaction_created_on_connection_notification connection = stub(transaction_open?: false) connection.expects(:begin_transaction).with(joinable: false) + pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec)) + pool.stubs(:lock_thread=).with(false) fire_connection_notification(connection) end @@ -635,12 +665,16 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase # Mocha is not thread-safe so define our own stub to test connection = Class.new do attr_accessor :rollback_transaction_called + attr_accessor :pool def transaction_open?; true; end def begin_transaction(*args); end def rollback_transaction(*args) @rollback_transaction_called = true end end.new + connection.pool = Class.new do + def lock_thread=(lock_thread); false; end + end.new fire_connection_notification(connection) teardown_fixtures assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not") @@ -750,7 +784,7 @@ end class FasterFixturesTest < ActiveRecord::TestCase self.use_transactional_tests = false - fixtures :categories, :authors + fixtures :categories, :authors, :author_addresses def load_extra_fixture(name) fixture = create_fixtures(name).first @@ -948,7 +982,7 @@ end class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase ActiveRecord::FixtureSet.reset_cache - set_fixture_class :randomly_named_a9 => + set_fixture_class :randomly_named_a9 => ClassNameThatDoesNotFollowCONVENTIONS, :'admin/randomly_named_a9' => Admin::ClassNameThatDoesNotFollowCONVENTIONS1, @@ -1000,7 +1034,7 @@ end class FixtureClassNamesTest < ActiveRecord::TestCase def setup - @saved_cache = self.fixture_class_names.dup + @saved_cache = fixture_class_names.dup end def teardown @@ -1011,3 +1045,16 @@ class FixtureClassNamesTest < ActiveRecord::TestCase assert_nil fixture_class_names["unregistered_identifier"] end end + +class SameNameDifferentDatabaseFixturesTest < ActiveRecord::TestCase + fixtures :dogs, :other_dogs + + test "fixtures are properly loaded" do + # Force loading the fixtures again to reproduce issue + ActiveRecord::FixtureSet.reset_cache + create_fixtures("dogs", "other_dogs") + + assert_kind_of Dog, dogs(:sophie) + assert_kind_of OtherDog, other_dogs(:lassie) + end +end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index 75c3493527..ffa3f63e0d 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -144,7 +144,7 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end def test_strong_params_style_objects_work_with_singular_associations - params = ProtectedParams.new( name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! + params = ProtectedParams.new(name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! part = ShipPart.new(params) assert_equal "Stern", part.name @@ -155,7 +155,7 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase params = ProtectedParams.new( trinkets_attributes: ProtectedParams.new( "0" => ProtectedParams.new(name: "Necklace").permit!, - "1" => ProtectedParams.new(name: "Spoon").permit! ) ).permit! + "1" => ProtectedParams.new(name: "Spoon").permit!)).permit! part = ShipPart.new(params) assert_equal "Necklace", part.trinkets[0].name diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f1d69a215a..5a3b8e3fb5 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -6,7 +6,6 @@ require "active_record" require "cases/test_case" require "active_support/dependencies" require "active_support/logger" -require "active_support/core_ext/string/strip" require "support/config" require "support/connection" @@ -27,9 +26,6 @@ ARTest.connect # Quote "type" if it's a reserved word for the current connection. QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type") -# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends. -ActiveRecord::Base.time_zone_aware_types << :time - def current_adapter?(*types) types.any? do |type| ActiveRecord::ConnectionAdapters.const_defined?(type) && diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 9ad4664567..b4bbdc6dad 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -58,21 +58,21 @@ class InheritanceTest < ActiveRecord::TestCase end def test_compute_type_success - assert_equal Author, ActiveRecord::Base.send(:compute_type, "Author") + assert_equal Author, Company.send(:compute_type, "Author") end def test_compute_type_nonexistent_constant e = assert_raises NameError do - ActiveRecord::Base.send :compute_type, "NonexistentModel" + Company.send :compute_type, "NonexistentModel" end - assert_equal "uninitialized constant ActiveRecord::Base::NonexistentModel", e.message - assert_equal "ActiveRecord::Base::NonexistentModel", e.name + assert_equal "uninitialized constant Company::NonexistentModel", e.message + assert_equal "Company::NonexistentModel", e.name end def test_compute_type_no_method_error ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise NoMethodError }) do assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end @@ -90,7 +90,7 @@ class InheritanceTest < ActiveRecord::TestCase ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end assert_equal error.message, exception.message end @@ -99,7 +99,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_compute_type_argument_error ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise ArgumentError }) do assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end @@ -316,7 +316,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_new_with_autoload_paths - path = File.expand_path("../../models/autoloadable", __FILE__) + path = File.expand_path("../models/autoloadable", __dir__) ActiveSupport::Dependencies.autoload_paths << path firm = Company.new(type: "ExtraFirm") @@ -417,7 +417,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do + assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(includes: :firm).find(1) end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 766917b196..9104976126 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,4 +1,3 @@ - require "cases/helper" require "models/company" require "models/developer" @@ -15,7 +14,7 @@ class IntegrationTest < ActiveRecord::TestCase def test_to_param_returns_nil_if_not_persisted client = Client.new - assert_equal nil, client.to_param + assert_nil client.to_param end def test_to_param_returns_id_if_not_persisted_but_id_is_set @@ -89,7 +88,7 @@ class IntegrationTest < ActiveRecord::TestCase def test_to_param_class_method_uses_default_if_not_persisted firm = Firm.new(name: "Fancy Shirts") - assert_equal nil, firm.to_param + assert_nil firm.to_param end def test_to_param_with_no_arguments @@ -169,7 +168,65 @@ class IntegrationTest < ActiveRecord::TestCase end def test_named_timestamps_for_cache_key - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + assert_deprecated do + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + end + end + + def test_cache_key_when_named_timestamp_is_nil + assert_deprecated do + owner = owners(:blackbeard) + owner.happy_at = nil + assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + end + end + + def test_cache_key_is_stable_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key + + developer.touch + second_key = developer.cache_key + + assert_equal first_key, second_key + ensure + Developer.cache_versioning = false + end + + def test_cache_version_changes_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_version = developer.cache_version + + travel 10.seconds do + developer.touch + end + + second_version = developer.cache_version + + assert_not_equal first_version, second_version + ensure + Developer.cache_versioning = false + end + + def test_cache_key_retains_version_when_custom_timestamp_is_used + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key_with_version + + travel 10.seconds do + developer.touch + end + + second_key = developer.cache_key_with_version + + assert_not_equal first_key, second_key + ensure + Developer.cache_versioning = false end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 9d5aace7db..cc3951e2ba 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -165,10 +165,8 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - ActiveSupport::Deprecation.silence do - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) - end + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) end end ActiveRecord::Migration.verbose = @verbose_was @@ -199,14 +197,14 @@ module ActiveRecord def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } + assert migration.connection.table_exists?("horses"), "horses should exist" end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") end def test_migrate_revert @@ -214,11 +212,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") revert.migrate :down - ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } + assert migration.connection.table_exists?("horses") migration.migrate :down - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") end def test_migrate_revert_by_part @@ -226,24 +224,18 @@ module ActiveRecord received = [] migration = InvertibleByPartsMigration.new migration.test = ->(dir) { - ActiveSupport::Deprecation.silence do - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") - end + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") received << dir } migration.migrate :up assert_equal [:both, :up], received - ActiveSupport::Deprecation.silence do - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") - end + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") migration.migrate :down assert_equal [:both, :up, :both, :down], received - ActiveSupport::Deprecation.silence do - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") - end + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") end def test_migrate_revert_whole_migration @@ -252,20 +244,20 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") revert.migrate :down - ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } + assert migration.connection.table_exists?("horses") migration.migrate :down - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } + assert revert.connection.table_exists?("horses") revert.migrate :up - ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } + assert !revert.connection.table_exists?("horses") end def test_migrate_revert_change_column_default @@ -330,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end def test_up LegacyMigration.up - ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" end def test_down LegacyMigration.up LegacyMigration.down - ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end def test_migrate_down_with_table_name_prefix @@ -356,7 +348,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } + assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = "" end diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index b06fed4f0d..9b4b61b16e 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -101,8 +101,19 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + def test_uses_serializable_hash_with_frozen_hash + def @contact.serializable_hash(options = nil) + super({ only: %w(name) }.freeze) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{awesome}, json + assert_no_match %r{age}, json + end + def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) + def @contact.serializable_hash(options = nil) super(only: %w(name)) end @@ -113,7 +124,7 @@ class JsonSerializationTest < ActiveRecord::TestCase end def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) + def @contact.serializable_hash(options = nil) super(except: %w(age)) end @@ -137,7 +148,7 @@ class JsonSerializationTest < ActiveRecord::TestCase @contact = ContactSti.new(@contact.attributes) assert_equal "ContactSti", @contact.type - def @contact.serializable_hash(options={}) + def @contact.serializable_hash(options = {}) super({ except: %w(age) }.merge!(options)) end @@ -157,7 +168,7 @@ class JsonSerializationTest < ActiveRecord::TestCase end class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase - fixtures :authors, :posts, :comments, :tags, :taggings + fixtures :authors, :author_addresses, :posts, :comments, :tags, :taggings include JsonSerializationHelpers @@ -243,7 +254,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase assert !@david.posts.first.respond_to?(:favorite_quote) assert_match %r{"favorite_quote":"Constraints are liberating"}, json - assert_equal %r{"favorite_quote":}.match(json).size, 1 + assert_equal 1, %r{"favorite_quote":}.match(json).size end def test_should_allow_only_option_for_list_of_authors diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb new file mode 100644 index 0000000000..ef5ca86874 --- /dev/null +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -0,0 +1,188 @@ +require "support/schema_dumping_helper" + +module JSONSharedTestCases + include SchemaDumpingHelper + + class JsonDataType < ActiveRecord::Base + self.table_name = "json_data_type" + + store_accessor :settings, :resolution + end + + def test_column + column = JsonDataType.columns_hash["payload"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table("json_data_type") do |t| + t.public_send column_type, "users" + end + JsonDataType.reset_column_information + column = JsonDataType.columns_hash["users"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.#{column_type}\s+"settings"/, output) + end + + def test_cast_value_on_write + x = JsonDataType.new(payload: { "string" => "foo", :symbol => :bar }) + assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) + x.save! + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) + end + + def test_type_cast_json + type = JsonDataType.type_for_attribute("payload") + + data = '{"a_key":"a_value"}' + hash = type.deserialize(data) + assert_equal({ "a_key" => "a_value" }, hash) + assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = JsonDataType.first + x.payload = { '"a\'' => "b" } + assert x.save! + end + + def test_select + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = JsonDataType.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]}')|) + x = JsonDataType.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)") + x = JsonDataType.first + assert_nil(x.payload) + end + + def test_select_nil_json_after_create + json = JsonDataType.create!(payload: nil) + x = JsonDataType.where(payload: nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = JsonDataType.create!(payload: "foo") + x = JsonDataType.where(payload: nil).first + assert_nil(x) + + json.update_attributes(payload: nil) + x = JsonDataType.where(payload: nil).first + assert_equal(json.reload, x) + end + + def test_select_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = JsonDataType.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"}]')|) + x = JsonDataType.first + x.payload = ["v1", { "k2" => "v2" }, "v3"] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { "one" => "two" } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload["three"] = "four" + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ "one" => "two", "three" => "four" }, json.payload) + assert_not json.changed? + end + + def test_changes_in_place_with_ruby_object + time = Time.now.utc + json = JsonDataType.create!(payload: time) + + json.reload + assert_not json.changed? + + json.payload = time + assert_not json.changed? + end + + def test_assigning_string_literal + json = JsonDataType.create!(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = JsonDataType.create!(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = JsonDataType.create!(payload: true) + assert_equal true, json.payload + end +end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 13b6f6daaf..3a3b8e51f9 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -18,7 +18,7 @@ class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.table_name = :lock_without_defaults_cust - self.column_defaults # to test @column_defaults caching. + column_defaults # to test @column_defaults caching. self.locking_column = :custom_lock_version end @@ -161,14 +161,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal(error.record.object_id, p2.object_id) end - def test_lock_new_with_nil - p1 = Person.new(first_name: "anika") - p1.save! - p1.lock_version = nil # simulate bad fixture or column with no default - p1.save! - assert_equal 1, p1.lock_version - end - def test_lock_new_when_explicitly_passing_nil p1 = Person.new(first_name: "anika", lock_version: nil) p1.save! @@ -222,22 +214,115 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_lock_without_default_sets_version_to_zero t1 = LockWithoutDefault.new + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.save! + t1.reload + + assert_equal 0, t1.lock_version + assert_equal 0, t1.lock_version_before_type_cast + end + + def test_lock_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + t2 = LockWithoutDefault.last + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + assert_equal 0, t2.lock_version + assert_nil t2.lock_version_before_type_cast + + t1.title = "new title1" + t2.title = "new title2" - t1.save - t1 = LockWithoutDefault.find(t1.id) + assert_nothing_raised { t1.save! } + assert_equal 1, t1.lock_version + assert_equal "new title1", t1.title + + assert_raise(ActiveRecord::StaleObjectError) { t2.save! } + assert_equal 0, t2.lock_version + assert_equal "new title2", t2.title + end + + def test_lock_without_default_queries_count + t1 = LockWithoutDefault.create(title: "title1") + + assert_equal "title1", t1.title assert_equal 0, t1.lock_version + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, t1.lock_version + + t2 = LockWithoutDefault.new(title: "title1") + + assert_queries(1) { t2.save! } + + t2.reload + assert_equal "title1", t2.title + assert_equal 0, t2.lock_version end def test_lock_with_custom_column_without_default_sets_version_to_zero t1 = LockWithCustomColumnWithoutDefault.new + assert_equal 0, t1.custom_lock_version assert_nil t1.custom_lock_version_before_type_cast t1.save! t1.reload + + assert_equal 0, t1.custom_lock_version + assert_equal 0, t1.custom_lock_version_before_type_cast + end + + def test_lock_with_custom_column_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')") + + t1 = LockWithCustomColumnWithoutDefault.last + t2 = LockWithCustomColumnWithoutDefault.last + + assert_equal 0, t1.custom_lock_version + assert_nil t1.custom_lock_version_before_type_cast + assert_equal 0, t2.custom_lock_version + assert_nil t2.custom_lock_version_before_type_cast + + t1.title = "new title1" + t2.title = "new title2" + + assert_nothing_raised { t1.save! } + assert_equal 1, t1.custom_lock_version + assert_equal "new title1", t1.title + + assert_raise(ActiveRecord::StaleObjectError) { t2.save! } + assert_equal 0, t2.custom_lock_version + assert_equal "new title2", t2.title + end + + def test_lock_with_custom_column_without_default_queries_count + t1 = LockWithCustomColumnWithoutDefault.create(title: "title1") + + assert_equal "title1", t1.title assert_equal 0, t1.custom_lock_version - assert [0, "0"].include?(t1.custom_lock_version_before_type_cast) + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, t1.custom_lock_version + + t2 = LockWithCustomColumnWithoutDefault.new(title: "title1") + + assert_queries(1) { t2.save! } + + t2.reload + assert_equal "title1", t2.title + assert_equal 0, t2.custom_lock_version end def test_readonly_attributes @@ -351,7 +436,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase private - def add_counter_column_to(model, col="test_count") + def add_counter_column_to(model, col = "test_count") model.connection.add_column model.table_name, col, :integer, null: false, default: 0 model.reset_column_information end @@ -417,7 +502,10 @@ unless in_memory_db? Person.transaction do person = Person.find 1 old, person.first_name = person.first_name, "fooman" - person.lock! + # Locking a dirty record is deprecated + assert_deprecated do + person.lock! + end assert_equal old, person.first_name end end @@ -460,7 +548,8 @@ unless in_memory_db? assert first.end > second.end end - protected + private + def duel(zzz = 5) t0, t1, t2, t3 = nil, nil, nil, nil diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 90ad970e16..b80257962c 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -21,6 +21,7 @@ class LogSubscriberTest < ActiveRecord::TestCase TRANSACTION: REGEXP_CYAN, OTHER: REGEXP_MAGENTA } + Event = Struct.new(:duration, :payload) class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -55,25 +56,22 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_schema_statements_are_ignored - event = Struct.new(:duration, :payload) - logger = TestDebugLogSubscriber.new assert_equal 0, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!")) + logger.sql(Event.new(0.9, sql: "hi mom!")) assert_equal 1, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!", name: "foo")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "foo")) assert_equal 2, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!", name: "SCHEMA")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "SCHEMA")) assert_equal 2, logger.debugs.length end def test_sql_statements_are_not_squeezed - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new - logger.sql(event.new(0, sql: "ruby rails")) + logger.sql(Event.new(0.9, sql: "ruby rails")) assert_match(/ruby rails/, logger.debugs.first) end @@ -86,56 +84,51 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_basic_query_logging_coloration - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, color_regex| - logger.sql(event.new(0, sql: verb.to_s)) + logger.sql(Event.new(0.9, sql: verb.to_s)) assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_basic_payload_name_logging_coloration_generic_sql - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(event.new(0, sql: verb.to_s)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "SQL")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "SQL")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_basic_payload_name_logging_coloration_named_sql - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(event.new(0, sql: verb.to_s, name: "Model Load")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Load")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "Model Exists")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Exists")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "ANY SPECIFIC NAME")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "ANY SPECIFIC NAME")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_query_logging_coloration_with_nested_select - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| - logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_query_logging_coloration_with_multi_line_nested_select - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| @@ -145,13 +138,12 @@ class LogSubscriberTest < ActiveRecord::TestCase SELECT ID FROM THINGS ) EOS - logger.sql(event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end end def test_query_logging_coloration_with_lock - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true sql = <<-EOS @@ -159,14 +151,14 @@ class LogSubscriberTest < ActiveRecord::TestCase (SELECT * FROM mytable FOR UPDATE) ss WHERE col1 = 5; EOS - logger.sql(event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) sql = <<-EOS LOCK TABLE films IN SHARE MODE; EOS - logger.sql(event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end def test_exists_query_logging diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index bdb90eaa74..1d305fa11f 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -43,7 +43,7 @@ module ActiveRecord t.column :foo, :string, null: false end - assert_raises(ActiveRecord::StatementInvalid) do + assert_raises(ActiveRecord::NotNullViolation) do connection.execute "insert into testings (foo) values (NULL)" end end @@ -233,7 +233,7 @@ module ActiveRecord end connection.add_column :testings, :bar, :string, null: false - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::NotNullViolation) do connection.execute "insert into testings (foo, bar) values ('hello', NULL)" end end @@ -244,12 +244,16 @@ module ActiveRecord t.column :foo, :string end - con = connection - connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')" - assert_nothing_raised { connection.add_column :testings, :bar, :string, null: false, default: "default" } + quoted_id = connection.quote_column_name("id") + quoted_foo = connection.quote_column_name("foo") + quoted_bar = connection.quote_column_name("bar") + connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}) values (1, 'hello')") + assert_nothing_raised do + connection.add_column :testings, :bar, :string, null: false, default: "default" + end - assert_raises(ActiveRecord::StatementInvalid) do - connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" + assert_raises(ActiveRecord::NotNullViolation) do + connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}, #{quoted_bar}) values (2, 'hello', NULL)") end end @@ -265,6 +269,8 @@ module ActiveRecord if current_adapter?(:PostgreSQLAdapter) assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type + elsif current_adapter?(:Mysql2Adapter) + assert_equal "timestamp", klass.columns_hash["foo"].sql_type else assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type end @@ -405,9 +411,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } + assert connection.table_exists?(:testings) connection.drop_table(:testings, if_exists: true) - ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } + assert_not connection.table_exists?(:testings) end def test_drop_table_if_exists_nothing_raised diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 03d781d3d2..48df931543 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -72,9 +72,7 @@ module ActiveRecord assert_kind_of BigDecimal, row.wealth # If this assert fails, that means the SELECT is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end + assert_equal correct_value, row.wealth # Reset to old state TestModel.delete_all @@ -165,7 +163,7 @@ module ActiveRecord assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 } unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, limit: 0xfffffffff } + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } end end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 55c06da411..2329888345 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -225,6 +225,16 @@ module ActiveRecord 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? + + change_column "test_models", "contributor", :boolean, default: nil, null: false + TestModel.reset_column_information + assert_not 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? diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 802a969cb7..007926f1b9 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -211,11 +211,6 @@ module ActiveRecord assert_equal [:remove_index, [:table, { name: "new_index" }]], remove end - def test_invert_add_index_with_no_options - remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] - assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove - end - def test_invert_remove_index add = @recorder.inverse_of :remove_index, [:table, :one] assert_equal [:add_index, [:table, :one]], add diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 0a4b604601..7a80bfb899 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "support/schema_dumping_helper" module ActiveRecord class Migration @@ -55,7 +56,7 @@ module ActiveRecord end def test_references_does_not_add_index_by_default - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) create_table :more_testings do |t| t.references :foo @@ -73,7 +74,7 @@ module ActiveRecord end def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) create_table :more_testings do |t| t.timestamps @@ -90,7 +91,7 @@ module ActiveRecord end def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) add_timestamps :testings end @@ -102,17 +103,119 @@ module ActiveRecord assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null end - def test_legacy_migrations_get_deprecation_warning_when_run - migration = Class.new(ActiveRecord::Migration) { - def up - add_column :testings, :baz, :string - end - } + def test_legacy_migrations_raises_exception_when_inherited + e = assert_raises(StandardError) do + class_eval("class LegacyMigration < ActiveRecord::Migration; end") + end + assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) + end + end + end +end - assert_deprecated do - migration.migrate :up +class LegacyPrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class LegacyPrimaryKey < ActiveRecord::Base + end + + def setup + @migration = nil + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + @migration.migrate(:down) if @migration + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + LegacyPrimaryKey.reset_column_information + end + + def test_legacy_primary_key_should_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys do |t| + t.references :legacy_ref end end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert_not legacy_pk.bigint? + assert_not legacy_pk.null + + legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] + assert_not legacy_ref.bigint? + + record1 = LegacyPrimaryKey.create! + assert_not_nil record1.id + + record1.destroy + + record2 = LegacyPrimaryKey.create! + assert_not_nil record2.id + assert_operator record2.id, :>, record1.id + end + + def test_legacy_integer_primary_key_should_not_be_auto_incremented + skip if current_adapter?(:SQLite3Adapter) + + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :integer do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema + end + + if current_adapter?(:Mysql2Adapter) + def test_legacy_bigint_primary_key_should_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :bigint + end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert legacy_pk.bigint? + assert legacy_pk.auto_increment? + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema + end + else + def test_legacy_bigint_primary_key_should_not_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :bigint do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema end end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index f14d68f12b..c4896f3d6e 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,9 +12,7 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - ActiveSupport::Deprecation.silence do - connection.drop_table table_name if connection.table_exists?(table_name) - end + connection.drop_table table_name, if_exists: true end end @@ -80,55 +78,66 @@ module ActiveRecord assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns) end + def test_create_join_table_respects_reference_key_type + connection.create_join_table :artists, :musics do |t| + t.references :video + end + + artist_id, music_id, video_id = connection.columns(:artists_musics).sort_by(&:name) + + assert_equal video_id.sql_type, artist_id.sql_type + assert_equal video_id.sql_type, music_id.sql_type + end + def test_drop_join_table connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") } + assert !connection.table_exists?("artists_musics") end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table "artists", "musics" - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") } + assert !connection.table_exists?("artists_musics") end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("musics_videos") } + assert !connection.table_exists?("musics_videos") end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("catalog") } + assert !connection.table_exists?("catalog") end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: "catalog" connection.drop_join_table :artists, :musics, table_name: "catalog" - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("catalog") } + assert !connection.table_exists?("catalog") end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: { null: true } connection.drop_join_table :artists, :musics, column_options: { null: true } - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") } + assert !connection.table_exists?("artists_musics") end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table "audio_artists", "audio_musics" - ActiveSupport::Deprecation.silence { assert connection.table_exists?("audio_artists_musics") } + assert connection.table_exists?("audio_artists_musics") connection.drop_join_table "audio_artists", "audio_musics" - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" } + assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index cab2069754..7762d37915 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,12 +1,30 @@ require "cases/helper" -require "support/ddl_helper" require "support/schema_dumping_helper" +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? + module ActiveRecord + class Migration + class ForeignKeyInCreateTest < ActiveRecord::TestCase + def test_foreign_keys + foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter) + end + end + end + end +end + if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyTest < ActiveRecord::TestCase - include DdlHelper include SchemaDumpingHelper include ActiveSupport::Testing::Stream @@ -29,10 +47,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end teardown do - if defined?(@connection) - @connection.drop_table "astronauts", if_exists: true - @connection.drop_table "rockets", if_exists: true - end + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true end def test_foreign_keys @@ -76,20 +92,23 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end def test_add_foreign_key_with_non_standard_primary_key - with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do - @connection.add_foreign_key(:astronauts, :space_shuttles, - column: "rocket_id", primary_key: "pk", name: "custom_pk") + @connection.create_table :space_shuttles, id: false, force: true do |t| + t.bigint :pk, primary_key: true + end - foreign_keys = @connection.foreign_keys("astronauts") - assert_equal 1, foreign_keys.size + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") - fk = foreign_keys.first - assert_equal "astronauts", fk.from_table - assert_equal "space_shuttles", fk.to_table - assert_equal "pk", fk.primary_key + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size - @connection.remove_foreign_key :astronauts, name: "custom_pk" - end + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + ensure + @connection.remove_foreign_key :astronauts, name: "custom_pk" + @connection.drop_table :space_shuttles end def test_add_on_delete_restrict_foreign_key @@ -101,7 +120,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? fk = foreign_keys.first if current_adapter?(:Mysql2Adapter) # ON DELETE RESTRICT is the default on MySQL - assert_equal nil, fk.on_delete + assert_nil fk.on_delete else assert_equal :restrict, fk.on_delete end @@ -229,7 +248,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table("cities") { |t| } create_table("houses") do |t| - t.column :city_id, :integer + t.references :city end add_foreign_key :houses, :cities, column: "city_id" @@ -261,7 +280,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table(:schools) create_table(:classes) do |t| - t.column :school_id, :integer + t.references :school end add_foreign_key :classes, :schools end @@ -305,9 +324,11 @@ else @connection.remove_foreign_key :clubs, :categories end - def test_foreign_keys_should_raise_not_implemented - assert_raises NotImplementedError do - @connection.foreign_keys("clubs") + unless current_adapter?(:SQLite3Adapter) + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 0f975026b8..f10fcf1398 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -31,9 +31,10 @@ module ActiveRecord connection.add_index(table_name, [:foo], name: "old_idx") connection.rename_index(table_name, "old_idx", "new_idx") - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert_not connection.index_name_exists?(table_name, "old_idx", false) - assert connection.index_name_exists?(table_name, "new_idx", true) + assert_deprecated do + assert_not connection.index_name_exists?(table_name, "old_idx", false) + assert connection.index_name_exists?(table_name, "new_idx", true) + end end def test_rename_index_too_long @@ -45,8 +46,7 @@ module ActiveRecord } assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert connection.index_name_exists?(table_name, "old_idx", false) + assert connection.index_name_exists?(table_name, "old_idx") end def test_double_add_index @@ -63,7 +63,7 @@ module ActiveRecord def test_add_index_works_with_long_index_names connection.add_index(table_name, "foo", name: good_index_name) - assert connection.index_name_exists?(table_name, good_index_name, false) + assert connection.index_name_exists?(table_name, good_index_name) connection.remove_index(table_name, name: good_index_name) end @@ -75,7 +75,7 @@ module ActiveRecord } assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) - assert_not connection.index_name_exists?(table_name, too_long_index_name, false) + assert_not connection.index_name_exists?(table_name, too_long_index_name) connection.add_index(table_name, "foo", name: good_index_name) end @@ -83,7 +83,7 @@ module ActiveRecord good_index_name = "x" * connection.index_name_length connection.add_index(table_name, "foo", name: good_index_name, internal: true) - assert connection.index_name_exists?(table_name, good_index_name, false) + assert connection.index_name_exists?(table_name, good_index_name) connection.remove_index(table_name, name: good_index_name) end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 61f5a061b0..6970fdcc87 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -21,8 +21,6 @@ module ActiveRecord end def test_errors_if_pending - @connection.expect :supports_migrations?, true - ActiveRecord::Migrator.stub :needs_migration?, true do assert_raise ActiveRecord::PendingMigrationError do @pending.call(nil) @@ -31,22 +29,12 @@ module ActiveRecord end def test_checks_if_supported - @connection.expect :supports_migrations?, true @app.expect :call, nil, [:foo] ActiveRecord::Migrator.stub :needs_migration?, false do @pending.call(:foo) end end - - def test_doesnt_check_if_unsupported - @connection.expect :supports_migrations?, false - @app.expect :call, nil, [:foo] - - ActiveRecord::Migrator.stub :needs_migration?, true do - @pending.call(:foo) - end - end end end end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 528811db49..f1ddac1ee2 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -1,9 +1,9 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_foreign_keys? +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? module ActiveRecord class Migration - class ReferencesForeignKeyTest < ActiveRecord::TestCase + class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table(:testing_parents, force: true) @@ -42,8 +42,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "options hash can be passed" do @connection.change_table :testing_parents do |t| - t.integer :other_id - t.index :other_id, unique: true + t.references :other, index: { unique: true } end @connection.create_table :testings do |t| t.references :testing_parent, foreign_key: { primary_key: :other_id } @@ -61,6 +60,24 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal([["testings", "testing_parents", "parent_id"]], fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }) end + end + end + end +end + +if ActiveRecord::Base.connection.supports_foreign_keys? + module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end test "foreign keys cannot be added to polymorphic relations when creating the table" do @connection.create_table :testings do |t| @@ -92,8 +109,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "foreign keys accept options when changing the table" do @connection.change_table :testing_parents do |t| - t.integer :other_id - t.index :other_id, unique: true + t.references :other, index: { unique: true } end @connection.create_table :testings @connection.change_table :testings do |t| @@ -177,18 +193,31 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "multiple foreign keys can be added to the same table" do @connection.create_table :testings do |t| - t.integer :col_1 - t.integer :col_2 + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } + end - t.foreign_key :testing_parents, column: :col_1 - t.foreign_key :testing_parents, column: :col_2 + fks = @connection.foreign_keys("testings").sort_by(&:column) + + fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "parent1_id"], + ["testings", "testing_parents", "parent2_id"]], fk_definitions) + end + + test "multiple foreign keys can be removed to the selected one" do + @connection.create_table :testings do |t| + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } end - fks = @connection.foreign_keys("testings") + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :parent1, foreign_key: { to_table: :testing_parents } + end + + fks = @connection.foreign_keys("testings").sort_by(&:column) fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } - assert_equal([["testings", "testing_parents", "col_1"], - ["testings", "testing_parents", "col_2"]], fk_definitions) + assert_equal([["testings", "testing_parents", "parent2_id"]], fk_definitions) end end end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 8fbe60f24e..e9eb9968cb 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -50,6 +50,21 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string, default: "Photo") end + def test_creates_reference_type_column_with_not_null + connection.create_table table_name, force: true do |t| + t.references :taggable, null: false, polymorphic: true + end + assert column_exists?(table_name, :taggable_id, :integer, null: false) + assert column_exists?(table_name, :taggable_type, :string, null: false) + end + + def test_does_not_share_options_with_reference_type_column + add_reference table_name, :taggable, type: :integer, limit: 2, polymorphic: true + assert column_exists?(table_name, :taggable_id, :integer, limit: 2) + assert column_exists?(table_name, :taggable_type, :string) + assert_not column_exists?(table_name, :taggable_type, :string, limit: 2) + end + def test_creates_named_index add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id" } assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id") @@ -57,7 +72,7 @@ module ActiveRecord def test_creates_named_unique_index add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id", unique: true } - assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id", unique: true ) + assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id", unique: true) end def test_creates_reference_id_with_specified_type diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index fc4f700916..5da3ad33a3 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } + rename_table :octopi, :test_models if connection.table_exists? :octopi super end @@ -79,10 +79,33 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + def test_renaming_table_renames_primary_key + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + rename_table :cats, :felines + + assert connection.table_exists? :felines + refute connection.table_exists? :cats + + primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + SELECT c.relname + FROM pg_class c + JOIN pg_index i + ON c.oid = i.indexrelid + WHERE i.indisprimary + AND i.indrelid = 'felines'::regclass + SQL + + assert_equal "felines_pkey", primary_key_name + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - connection.create_table :cats, id: :uuid + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } - ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } + assert connection.table_exists? :felines + refute 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 151f3c8efd..57f94950f9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -43,10 +43,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" - ActiveRecord::Base.connection.initialize_schema_migrations_table - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all - %w(things awesome_things prefix_things_suffix p_awesome_things_s ).each do |table| + %w(things awesome_things prefix_things_suffix p_awesome_things_s).each do |table| Thing.connection.drop_table(table) rescue nil end Thing.reset_column_information @@ -337,20 +337,20 @@ class MigrationTest < ActiveRecord::TestCase end def test_schema_migrations_table_name - original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name + original_schema_migrations_table_name = ActiveRecord::Base.schema_migrations_table_name - assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "schema_migrations", ActiveRecord::SchemaMigration.table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name - assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "prefix_schema_migrations_suffix", ActiveRecord::SchemaMigration.table_name ActiveRecord::Base.schema_migrations_table_name = "changed" Reminder.reset_table_name - assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "prefix_changed_suffix", ActiveRecord::SchemaMigration.table_name ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" Reminder.reset_table_name - assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "changed", ActiveRecord::SchemaMigration.table_name ensure ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name Reminder.reset_table_name @@ -388,7 +388,7 @@ class MigrationTest < ActiveRecord::TestCase original_rails_env = ENV["RAILS_ENV"] original_rack_env = ENV["RACK_ENV"] ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" - new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call refute_equal current_env, new_env @@ -399,44 +399,19 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env - end - - def test_migration_sets_internal_metadata_even_when_fully_migrated - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path - ActiveRecord::Migrator.up(migrations_path) - assert_equal current_env, ActiveRecord::InternalMetadata[:environment] - - original_rails_env = ENV["RAILS_ENV"] - original_rack_env = ENV["RACK_ENV"] - ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" - new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - - refute_equal current_env, new_env - - sleep 1 # mysql by default does not store fractional seconds in the database - - ActiveRecord::Migrator.up(migrations_path) - assert_equal new_env, ActiveRecord::InternalMetadata[:environment] - ensure - ActiveRecord::Migrator.migrations_paths = old_path - ENV["RAILS_ENV"] = original_rails_env - ENV["RACK_ENV"] = original_rack_env end def test_internal_metadata_stores_environment_when_other_data_exists ActiveRecord::InternalMetadata.delete_all - ActiveRecord::InternalMetadata[:foo] = "bar" + ActiveRecord::InternalMetadata[:foo] = "bar" 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 + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call ActiveRecord::Migrator.up(migrations_path) assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] @@ -705,7 +680,7 @@ class MigrationTest < ActiveRecord::TestCase end end - protected + private # This is needed to isolate class_attribute assignments like `table_name_prefix` # for each test case. def new_isolated_reminder_class @@ -887,7 +862,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal :datetime, column(:birthdate).type end - protected + private def with_bulk_change_table # Reset columns/indexes cache as we're changing the table @@ -914,7 +889,6 @@ if ActiveRecord::Base.connection.supports_bulk_alter? @indexes ||= Person.connection.indexes("delete_me") end end # AlterTableMigrationsTest - end class CopyMigrationsTest < ActiveRecord::TestCase @@ -1132,4 +1106,21 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_unknown_migration_version_should_raise_an_argument_error assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] } end + + def test_deprecate_initialize_internal_tables + assert_deprecated { ActiveRecord::Base.connection.initialize_schema_migrations_table } + assert_deprecated { ActiveRecord::Base.connection.initialize_internal_metadata_table } + end + + def test_deprecate_migration_keys + assert_deprecated { ActiveRecord::Base.connection.migration_keys } + end + + def test_deprecate_supports_migrations + assert_deprecated { ActiveRecord::Base.connection.supports_migrations? } + end + + def test_deprecate_schema_migrations_table_name + assert_deprecated { ActiveRecord::Migrator.schema_migrations_table_name } + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1ba18bc9c2..2e4b454a86 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -11,7 +11,7 @@ class MigratorTest < ActiveRecord::TestCase def initialize(name = self.class.name, version = nil) super - @went_up = false + @went_up = false @went_down = false end @@ -45,10 +45,11 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_with_duplicate_names - assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do + e = assert_raises(ActiveRecord::DuplicateMigrationNameError) do list = [ActiveRecord::Migration.new("Chunky"), ActiveRecord::Migration.new("Chunky")] ActiveRecord::Migrator.new(:up, list) end + assert_match(/Multiple migrations have the name Chunky/, e.message) end def test_migrator_with_duplicate_versions @@ -123,6 +124,67 @@ class MigratorTest < ActiveRecord::TestCase assert_equal migration_list.last, migrations.first end + def test_migrations_status + path = MIGRATIONS_ROOT + "/valid" + + ActiveRecord::SchemaMigration.create(version: 2) + ActiveRecord::SchemaMigration.create(version: 10) + + assert_equal [ + ["down", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["down", "003", "Innocent jointable"], + ["up", "010", "********** NO FILE **********"], + ], ActiveRecord::Migrator.migrations_status(path) + end + + def test_migrations_status_in_subdirectories + path = MIGRATIONS_ROOT + "/valid_with_subdirectories" + + ActiveRecord::SchemaMigration.create(version: 2) + ActiveRecord::SchemaMigration.create(version: 10) + + assert_equal [ + ["down", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["down", "003", "Innocent jointable"], + ["up", "010", "********** NO FILE **********"], + ], ActiveRecord::Migrator.migrations_status(path) + end + + def test_migrations_status_with_schema_define_in_subdirectories + path = MIGRATIONS_ROOT + "/valid_with_subdirectories" + prev_paths = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = path + + ActiveRecord::Schema.define(version: 3) do + end + + assert_equal [ + ["up", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["up", "003", "Innocent jointable"], + ], ActiveRecord::Migrator.migrations_status(path) + ensure + ActiveRecord::Migrator.migrations_paths = prev_paths + end + + def test_migrations_status_from_two_directories + paths = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] + + ActiveRecord::SchemaMigration.create(version: "20100101010101") + ActiveRecord::SchemaMigration.create(version: "20160528010101") + + assert_equal [ + ["down", "20090101010101", "People have hobbies"], + ["down", "20090101010202", "People have descriptions"], + ["up", "20100101010101", "Valid with timestamps people have last names"], + ["down", "20100201010101", "Valid with timestamps we need reminders"], + ["down", "20100301010101", "Valid with timestamps innocent jointable"], + ["up", "20160528010101", "********** NO FILE **********"], + ], ActiveRecord::Migrator.migrations_status(paths) + end + def test_migrator_interleaved_migrations pass_one = [Sensor.new("One", 1)] @@ -237,6 +299,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_verbosity _, migrations = sensors(3) + ActiveRecord::Migration.verbose = true ActiveRecord::Migrator.new(:up, migrations, 1).migrate assert_not_equal 0, ActiveRecord::Migration.message_count @@ -249,7 +312,6 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_verbosity_off _, migrations = sensors(3) - ActiveRecord::Migration.message_count = 0 ActiveRecord::Migration.verbose = false ActiveRecord::Migrator.new(:up, migrations, 1).migrate assert_equal 0, ActiveRecord::Migration.message_count @@ -290,6 +352,27 @@ class MigratorTest < ActiveRecord::TestCase assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls end + def test_migrator_output_when_running_multiple_migrations + _, migrator = migrator_class(3) + + result = migrator.migrate("valid") + assert_equal(3, result.count) + + # Nothing migrated from duplicate run + result = migrator.migrate("valid") + assert_equal(0, result.count) + + result = migrator.rollback("valid") + assert_equal(1, result.count) + end + + def test_migrator_output_when_running_single_migration + _, migrator = migrator_class(1) + result = migrator.run(:up, "valid", 1) + + assert_equal(1, result.version) + end + def test_migrator_rollback _, migrator = migrator_class(3) @@ -313,9 +396,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") } + assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") migrator.migrate("valid", 1) - ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("schema_migrations") } + assert ActiveRecord::Base.connection.table_exists?("schema_migrations") end def test_migrator_forward @@ -344,10 +427,10 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) migrator.migrate("valid") - assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) + assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions) migrator.rollback("valid") - assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) + assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions) migrator.rollback("valid") assert_equal([1], ActiveRecord::Migrator.get_all_versions) @@ -368,7 +451,7 @@ class MigratorTest < ActiveRecord::TestCase def sensors(count) calls = [] migrations = count.times.map { |i| - m(nil, i + 1) { |c,migration| + m(nil, i + 1) { |c, migration| calls << [c, migration.version] } } diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index b2f76398df..ceb5724377 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -260,6 +260,13 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time assert_not topic.bonus_time.utc? + + attributes = { + "written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "", "written_on(5i)" => "" + } + topic.attributes = attributes + assert_nil topic.written_on end ensure Topic.reset_column_information @@ -280,14 +287,14 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase unless current_adapter? :OracleAdapter def test_multiparameter_attributes_setting_time_attribute - topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + topic = Topic.new("bonus_time(4i)" => "01", "bonus_time(5i)" => "05") assert_equal 1, topic.bonus_time.hour assert_equal 5, topic.bonus_time.min end end def test_multiparameter_attributes_setting_date_attribute - topic = Topic.new( "written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11" ) + topic = Topic.new("written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11") assert_equal 1952, topic.written_on.year assert_equal 3, topic.written_on.month assert_equal 11, topic.written_on.day @@ -308,8 +315,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_setting_time_but_not_date_on_date_field - assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do - Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" ) + assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + Topic.new("written_on(4i)" => "13", "written_on(5i)" => "55") end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index a9c3733c20..5a62cbd3a6 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -752,7 +752,7 @@ module NestedAttributesOnACollectionAssociationTests exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") end - assert_equal 'Hash or Array expected, got String ("foo")', exception.message + assert_equal %{Hash or Array expected for attribute `#{@association_name}`, got String ("foo")}, exception.message end def test_should_work_with_update_as_well @@ -971,7 +971,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase def test_attr_accessor_of_child_should_be_value_provided_during_update @owner = owners(:ashley) @pet1 = pets(:chew) - attributes = { pets_attributes: { "1"=> { id: @pet1.id, + attributes = { pets_attributes: { "1" => { id: @pet1.id, name: "Foo2", current_user: "John", _destroy: true } } } @@ -1030,13 +1030,13 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR end test "if association is not loaded and association record is saved and then in memory record attributes should be saved" do - @ship.parts_attributes=[{ id: @part.id,name: "Deck" }] + @ship.parts_attributes = [{ id: @part.id, name: "Deck" }] assert_equal 1, @ship.association(:parts).target.size assert_equal "Deck", @ship.parts[0].name end test "if association is not loaded and child doesn't change and I am saving a grandchild then in memory record should be used" do - @ship.parts_attributes=[{ id: @part.id,trinkets_attributes: [{ id: @trinket.id, name: "Ruby" }] }] + @ship.parts_attributes = [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "Ruby" }] }] assert_equal 1, @ship.association(:parts).target.size assert_equal "Mast", @ship.parts[0].name assert_no_difference("@ship.parts[0].association(:trinkets).target.size") do diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index 8954e8c7e3..b9d2acbed2 100644 --- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -5,13 +5,13 @@ require "models/bird" class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase Pirate.has_many(:birds_with_add_load, class_name: "Bird", - before_add: proc { |p,b| + before_add: proc { |p, b| @@add_callback_called << b p.birds_with_add_load.to_a }) Pirate.has_many(:birds_with_add, class_name: "Bird", - before_add: proc { |p,b| @@add_callback_called << b }) + before_add: proc { |p, b| @@add_callback_called << b }) Pirate.accepts_nested_attributes_for(:birds_with_add_load, :birds_with_add, @@ -21,7 +21,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase @@add_callback_called = [] @pirate = Pirate.new.tap do |pirate| pirate.catchphrase = "Don't call me!" - pirate.birds_attributes = [{ name: "Bird1" },{ name: "Bird2" }] + pirate.birds_attributes = [{ name: "Bird1" }, { name: "Bird2" }] pirate.save! end @birds = @pirate.birds.to_a @@ -37,7 +37,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase def existing_birds_attributes @birds.map do |bird| - bird.attributes.slice("id","name") + bird.attributes.slice("id", "name") end end @@ -120,14 +120,14 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase assert_assignment_affects_records_in_target(:birds_with_add) end - test("Assignment updates records in target when not loaded" + + test("Assignment updates records in target when not loaded" \ " and callback loads target") do assert_not @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 - test("Assignment updates records in target when loaded" + + test("Assignment updates records in target when loaded" \ " and callback loads target") do @pirate.birds_with_add_load.load_target @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 688c3ed2b1..5895c51714 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -49,7 +49,7 @@ class PersistenceTest < ActiveRecord::TestCase assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception else # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead - assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do test_update_with_order_succeeds.call("id DESC") end end @@ -90,6 +90,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal count, Pet.joins(:toys).where(where_args).delete_all end + def test_delete_all_with_left_joins + where_args = { toys: { name: "Bone" } } + count = Pet.left_joins(:toys).where(where_args).count + + assert_equal count, 1 + assert_equal count, Pet.left_joins(:toys).where(where_args).delete_all + end + def test_delete_all_with_joins_and_where_part_is_not_hash where_args = ["toys.name = ?", "Bone"] count = Pet.joins(:toys).where(where_args).count @@ -131,6 +139,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal initial_credit + 2, a1.reload.credit_limit end + def test_increment_updates_timestamps + topic = topics(:first) + topic.update_columns(updated_at: 5.minutes.ago) + previous_updated_at = topic.updated_at + topic.increment!(:replies_count, touch: true) + assert_operator previous_updated_at, :<, topic.reload.updated_at + end + def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a @@ -222,6 +238,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 41, accounts(:signals37, :reload).credit_limit end + def test_decrement_updates_timestamps + topic = topics(:first) + topic.update_columns(updated_at: 5.minutes.ago) + previous_updated_at = topic.updated_at + topic.decrement!(:replies_count, touch: true) + assert_operator previous_updated_at, :<, topic.reload.updated_at + end + def test_create topic = Topic.new topic.title = "New Topic" @@ -453,6 +477,20 @@ class PersistenceTest < ActiveRecord::TestCase assert_nil Topic.find(2).last_read end + def test_update_all_with_joins + where_args = { toys: { name: "Bone" } } + count = Pet.left_joins(:toys).where(where_args).count + + assert_equal count, Pet.joins(:toys).where(where_args).update_all(name: "Bob") + end + + def test_update_all_with_left_joins + where_args = { toys: { name: "Bone" } } + count = Pet.left_joins(:toys).where(where_args).count + + assert_equal count, Pet.left_joins(:toys).where(where_args).update_all(name: "Bob") + end + def test_update_all_with_non_standard_table_name assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0]) assert_equal 0, WarehouseThing.find(1).value @@ -967,7 +1005,7 @@ class PersistenceTest < ActiveRecord::TestCase self.table_name = :widgets end - instance = widget.create!( + instance = widget.create!( name: "Bob", created_at: 1.day.ago, updated_at: 1.day.ago) diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 31d612abd1..5ded619716 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -7,6 +7,7 @@ require "models/movie" require "models/keyboard" require "models/mixed_case_monkey" require "models/dashboard" +require "models/non_primary_key" class PrimaryKeysTest < ActiveRecord::TestCase fixtures :topics, :subscribers, :movies, :mixed_case_monkeys @@ -89,6 +90,12 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal("John Doe", subscriberReloaded.name) end + def test_id_column_that_is_not_primary_key + NonPrimaryKey.create!(id: 100) + actual = NonPrimaryKey.find_by(id: 100) + assert_match %r{<NonPrimaryKey id: 100}, actual.inspect + end + def test_find_with_more_than_one_string_key assert_equal 2, Subscriber.find(subscribers(:first).nick, subscribers(:second).nick).length end @@ -113,48 +120,49 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_delete_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.delete(1) } end + def test_update_counters_should_quote_pkey_and_quote_counter_columns assert_nothing_raised { MixedCaseMonkey.update_counters(1, fleaCount: 99) } end + def test_find_with_one_id_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1) } end + def test_find_with_multiple_ids_should_quote_pkey - assert_nothing_raised { MixedCaseMonkey.find([1,2]) } + assert_nothing_raised { MixedCaseMonkey.find([1, 2]) } end + def test_instance_update_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1).save } end + def test_instance_destroy_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1).destroy } end - def test_supports_primary_key - assert_nothing_raised do - ActiveRecord::Base.connection.supports_primary_key? - end + def test_deprecate_supports_primary_key + assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? } end - if ActiveRecord::Base.connection.supports_primary_key? - def test_primary_key_returns_value_if_it_exists - klass = Class.new(ActiveRecord::Base) do - self.table_name = "developers" - end - - assert_equal "id", klass.primary_key + def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers" end - def test_primary_key_returns_nil_if_it_does_not_exist - klass = Class.new(ActiveRecord::Base) do - self.table_name = "developers_projects" - end + assert_equal "id", klass.primary_key + end - assert_nil klass.primary_key + def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers_projects" end + + assert_nil klass.primary_key end def test_quoted_primary_key_after_set_primary_key - k = Class.new( ActiveRecord::Base ) + k = Class.new(ActiveRecord::Base) assert_equal k.connection.quote_column_name("id"), k.quoted_primary_key k.primary_key = "foo" assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key @@ -175,6 +183,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_create_without_primary_key_no_extra_query + skip if current_adapter?(:OracleAdapter) + klass = Class.new(ActiveRecord::Base) do self.table_name = "dashboards" end @@ -216,6 +226,43 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyWithAutoIncrementTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class AutoIncrement < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table(:auto_increments, if_exists: true) + end + + def test_primary_key_with_integer + @connection.create_table(:auto_increments, id: :integer, force: true) + assert_auto_incremented + end + + def test_primary_key_with_bigint + @connection.create_table(:auto_increments, id: :bigint, force: true) + assert_auto_incremented + end + + private + def assert_auto_incremented + record1 = AutoIncrement.create! + assert_not_nil record1.id + + record1.destroy + + record2 = AutoIncrement.create! + assert_not_nil record2.id + assert_operator record2.id, :>, record1.id + end +end + class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -246,6 +293,14 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema end + + if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported? + test "schema typed primary key column" do + @connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true) + schema = dump_table_schema("scheduled_logs") + assert_match %r/create_table "scheduled_logs", id: :timestamp, precision: 6/, schema + end + end end class CompositePrimaryKeyTest < ActiveRecord::TestCase @@ -260,6 +315,10 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase t.string :region t.integer :code end + @connection.create_table(:barcodes_reverse, primary_key: ["code", "region"], force: true) do |t| + t.string :region + t.integer :code + end end def teardown @@ -270,6 +329,11 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_equal ["region", "code"], @connection.primary_keys("barcodes") end + def test_composite_primary_key_out_of_order + skip if current_adapter?(:SQLite3Adapter) + assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse") + end + def test_primary_key_issues_warning model = Class.new(ActiveRecord::Base) do def self.table_name @@ -282,42 +346,47 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_match(/WARNING: Active Record does not support composite primary key\./, warning) end - def test_collectly_dump_composite_primary_key + def test_dumping_composite_primary_key schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema end + + def test_dumping_composite_primary_key_out_of_order + skip if current_adapter?(:SQLite3Adapter) + schema = dump_table_schema "barcodes_reverse" + assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema + end end -if current_adapter?(:Mysql2Adapter) - class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase - include SchemaDumpingHelper +class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase + include SchemaDumpingHelper - self.use_transactional_tests = false + self.use_transactional_tests = false - def setup - @connection = ActiveRecord::Base.connection - @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true) - end + def setup + @connection = ActiveRecord::Base.connection + end - def teardown - @connection.drop_table :bigint_defaults, if_exists: true - end + def teardown + @connection.drop_table :int_defaults, if_exists: true + end - test "primary key with bigint allows default override via nil" do - column = @connection.columns(:bigint_defaults).find { |c| c.name == "id" } - assert column.bigint? - assert_not column.auto_increment? - end + def test_schema_dump_primary_key_integer_with_default_nil + skip if current_adapter?(:SQLite3Adapter) + @connection.create_table(:int_defaults, id: :integer, default: nil, force: true) + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema + end - test "schema dump primary key with bigint default nil" do - schema = dump_table_schema "bigint_defaults" - assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema - end + def test_schema_dump_primary_key_bigint_with_default_nil + @connection.create_table(:int_defaults, id: :bigint, default: nil, force: true) + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :bigint, default: nil}, schema end end if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) - class PrimaryKeyBigSerialTest < ActiveRecord::TestCase + class PrimaryKeyIntegerTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false @@ -327,46 +396,55 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) setup do @connection = ActiveRecord::Base.connection - if current_adapter?(:PostgreSQLAdapter) - @connection.create_table(:widgets, id: :bigserial, force: true) - else - @connection.create_table(:widgets, id: :bigint, force: true) - end + @pk_type = current_adapter?(:PostgreSQLAdapter) ? :serial : :integer end teardown do @connection.drop_table :widgets, if_exists: true - Widget.reset_column_information end - test "primary key column type with bigserial" do - column_type = Widget.type_for_attribute(Widget.primary_key) - assert_equal :integer, column_type.type - assert_equal 8, column_type.limit + test "primary key column type with serial/integer" do + @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? end - test "primary key with bigserial are automatically numbered" do + test "primary key with serial/integer are automatically numbered" do + @connection.create_table(:widgets, id: @pk_type, force: true) widget = Widget.create! assert_not_nil widget.id end - test "schema dump primary key with bigserial" do + test "schema dump primary key with serial/integer" do + @connection.create_table(:widgets, id: @pk_type, force: true) schema = dump_table_schema "widgets" - if current_adapter?(:PostgreSQLAdapter) - assert_match %r{create_table "widgets", id: :bigserial, force: :cascade}, schema - else - assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema - end + assert_match %r{create_table "widgets", id: :#{@pk_type}, force: :cascade}, schema end if current_adapter?(:Mysql2Adapter) test "primary key column type with options" do - @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true) + @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_equal :integer, column.type - assert_equal 8, column.limit + assert_not column.bigint? assert column.unsigned? + + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :integer, unsigned: true, force: :cascade}, schema + end + + 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_equal :integer, column.type + assert column.bigint? + assert column.unsigned? + + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :bigint, unsigned: true, force: :cascade}, schema end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 7f7faca70d..494663eb04 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -10,13 +10,34 @@ class QueryCacheTest < ActiveRecord::TestCase fixtures :tasks, :topics, :categories, :posts, :categories_posts - teardown do + class ShouldNotHaveExceptionsLogger < ActiveRecord::LogSubscriber + attr_reader :logger + + def initialize + super + @logger = ::Logger.new File::NULL + @exception = false + end + + def exception? + @exception + end + + def sql(event) + super + rescue + @exception = true + end + end + + def teardown Task.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! + super end def test_exceptional_middleware_clears_and_disables_cache_on_error - assert !ActiveRecord::Base.connection.query_cache_enabled, "cache off" + assert_cache :off mw = middleware { |env| Task.find 1 @@ -26,19 +47,92 @@ class QueryCacheTest < ActiveRecord::TestCase } assert_raises(RuntimeError) { mw.call({}) } - assert_equal 0, ActiveRecord::Base.connection.query_cache.length - assert !ActiveRecord::Base.connection.query_cache_enabled, "cache off" + assert_cache :off end - def test_exceptional_middleware_leaves_enabled_cache_alone - ActiveRecord::Base.connection.enable_query_cache! + private def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool - mw = middleware { |env| - raise "lol borked" - } - assert_raises(RuntimeError) { mw.call({}) } + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end + + def test_query_cache_across_threads + with_temporary_connection_pool do + begin + if in_memory_db? + # Separate connections to an in-memory database create an entirely new database, + # with an empty schema etc, so we just stub out this schema on the fly. + ActiveRecord::Base.connection_pool.with_connection do |connection| + connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending + end + end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end + + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end + + assert !ActiveRecord::Base.connection.nil? + assert_cache :off + + middleware { + assert_cache :clean + + Task.find 1 + assert_cache :dirty + + thread_1_connection = ActiveRecord::Base.connection + ActiveRecord::Base.clear_active_connections! + assert_cache :off, thread_1_connection + + started = Concurrent::Event.new + checked = Concurrent::Event.new - assert ActiveRecord::Base.connection.query_cache_enabled, "cache on" + thread_2_connection = nil + thread = Thread.new { + thread_2_connection = ActiveRecord::Base.connection + + assert_equal thread_2_connection, thread_1_connection + assert_cache :off + + middleware { + assert_cache :clean + + Task.find 1 + assert_cache :dirty + + started.set + checked.wait + + ActiveRecord::Base.clear_active_connections! + }.call({}) + } + + started.wait + + thread_1_connection = ActiveRecord::Base.connection + assert_not_equal thread_1_connection, thread_2_connection + assert_cache :dirty, thread_2_connection + checked.set + thread.join + + assert_cache :off, thread_2_connection + }.call({}) + + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end + ensure + ActiveRecord::Base.clear_all_connections! + end + end end def test_middleware_delegates @@ -62,10 +156,10 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_enabled_during_call - assert !ActiveRecord::Base.connection.query_cache_enabled, "cache off" + assert_cache :off mw = middleware { |env| - assert ActiveRecord::Base.connection.query_cache_enabled, "cache on" + assert_cache :clean [200, {}, nil] } mw.call({}) @@ -121,6 +215,33 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_cache_does_not_raise_exceptions + logger = ShouldNotHaveExceptionsLogger.new + subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger + + ActiveRecord::Base.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + + assert_not_predicate logger, :exception? + ensure + ActiveSupport::Notifications.unsubscribe subscriber + end + + def test_query_cache_does_not_allow_sql_key_mutation + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload| + payload[:sql].downcase! + end + + assert_raises RuntimeError do + ActiveRecord::Base.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + end + ensure + ActiveSupport::Notifications.unsubscribe subscriber + end + def test_cache_is_flat Task.cache do assert_queries(1) { Topic.find(1); Topic.find(1); } @@ -138,7 +259,7 @@ class QueryCacheTest < ActiveRecord::TestCase assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric - assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") + assert_instance_of 0.class, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") else assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") end @@ -165,19 +286,51 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_is_not_available_when_using_a_not_connected_connection - spec_name = Task.connection_specification_name - conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") - ActiveRecord::Base.connection_handler.establish_connection(conf) - Task.connection_specification_name = "test2" - refute Task.connected? + with_temporary_connection_pool do + spec_name = Task.connection_specification_name + conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") + ActiveRecord::Base.connection_handler.establish_connection(conf) + Task.connection_specification_name = "test2" + refute Task.connected? - Task.cache do - Task.connection # warmup postgresql connection setup queries - assert_queries(2) { Task.find(1); Task.find(1) } + Task.cache do + begin + if in_memory_db? + Task.connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending + end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end + Task.connection # warmup postgresql connection setup queries + assert_queries(2) { Task.find(1); Task.find(1) } + ensure + ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) + Task.connection_specification_name = spec_name + end + end + end + end + + def test_query_cache_executes_new_queries_within_block + ActiveRecord::Base.connection.enable_query_cache! + + # Warm up the cache by running the query + assert_queries(1) do + assert_equal 0, Post.where(title: "test").to_a.count + end + + # Check that if the same query is run again, no queries are executed + assert_queries(0) do + assert_equal 0, Post.where(title: "test").to_a.count + end + + ActiveRecord::Base.connection.uncached do + # Check that new query is executed, avoiding the cache + assert_queries(1) do + assert_equal 0, Post.where(title: "test").to_a.count + end end - ensure - ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) - Task.connection_specification_name = spec_name end def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries @@ -232,17 +385,78 @@ class QueryCacheTest < ActiveRecord::TestCase end end + 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 + + middleware { + refute 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" + 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 + + middleware { + assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + }.call({}) + end + end + + def test_query_caching_is_local_to_the_current_thread + with_temporary_connection_pool do + ActiveRecord::Base.clear_active_connections! + + middleware { + assert ActiveRecord::Base.connection_pool.query_cache_enabled + assert ActiveRecord::Base.connection.query_cache_enabled + + Thread.new { + refute ActiveRecord::Base.connection_pool.query_cache_enabled + refute ActiveRecord::Base.connection.query_cache_enabled + }.join + }.call({}) + + end + end + private def middleware(&app) executor = Class.new(ActiveSupport::Executor) ActiveRecord::QueryCache.install_executor_hooks executor lambda { |env| executor.wrap { app.call(env) } } end + + def assert_cache(state, connection = ActiveRecord::Base.connection) + case state + when :off + assert !connection.query_cache_enabled, "cache should be off" + assert connection.query_cache.empty?, "cache should be empty" + when :clean + assert connection.query_cache_enabled, "cache should be on" + assert connection.query_cache.empty?, "cache should be empty" + when :dirty + assert connection.query_cache_enabled, "cache should be on" + assert !connection.query_cache.empty?, "cache should be dirty" + else + raise "unknown state" + end + end end class QueryCacheExpiryTest < ActiveRecord::TestCase fixtures :tasks, :posts, :categories, :categories_posts + def teardown + Task.connection.clear_query_cache + end + def test_cache_gets_cleared_after_migration # warm the cache Post.find(1) @@ -318,4 +532,16 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end end end + + test "threads use the same connection" do + @connection_1 = ActiveRecord::Base.connection.object_id + + thread_a = Thread.new do + @connection_2 = ActiveRecord::Base.connection.object_id + end + + thread_a.join + + assert_equal @connection_1, @connection_2 + end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 296dafacc2..0819776fbf 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -81,53 +81,70 @@ module ActiveRecord end end + class QuotedOne + def quoted_id + 1 + end + end + class SubQuotedOne < QuotedOne + end def test_quote_with_quoted_id - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) + assert_deprecated(/defined on \S+::QuotedOne at .*quoting_test\.rb:[0-9]/) do + assert_equal 1, @quoter.quote(QuotedOne.new) + end + + assert_deprecated(/defined on \S+::SubQuotedOne\(\S+::QuotedOne\) at .*quoting_test\.rb:[0-9]/) do + assert_equal 1, @quoter.quote(SubQuotedOne.new) + end end def test_quote_nil - assert_equal "NULL", @quoter.quote(nil, nil) + assert_equal "NULL", @quoter.quote(nil) end def test_quote_true - assert_equal @quoter.quoted_true, @quoter.quote(true, nil) + assert_equal @quoter.quoted_true, @quoter.quote(true) end def test_quote_false - assert_equal @quoter.quoted_false, @quoter.quote(false, nil) + assert_equal @quoter.quoted_false, @quoter.quote(false) end def test_quote_float float = 1.2 - assert_equal float.to_s, @quoter.quote(float, nil) + assert_equal float.to_s, @quoter.quote(float) end def test_quote_integer integer = 1 - assert_equal integer.to_s, @quoter.quote(integer, nil) + assert_equal integer.to_s, @quoter.quote(integer) end def test_quote_bignum bignum = 1 << 100 - assert_equal bignum.to_s, @quoter.quote(bignum, nil) + assert_equal bignum.to_s, @quoter.quote(bignum) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) - assert_equal bigdec.to_s("F"), @quoter.quote(bigdec, nil) + assert_equal bigdec.to_s("F"), @quoter.quote(bigdec) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) "lol" end }) - assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, nil) + assert_equal "'lol'", @quoter.quote(Date.today) + assert_equal "'lol'", @quoter.quote(Time.now) + assert_equal "'lol'", @quoter.quote(DateTime.now) + end + + def test_quoting_classes + assert_equal "'Object'", @quoter.quote(Object) end def test_crazy_object crazy = Object.new e = assert_raises(TypeError) do - @quoter.quote(crazy, nil) + @quoter.quote(crazy) end assert_equal "can't quote Object", e.message end @@ -146,6 +163,62 @@ module ActiveRecord end end + class TypeCastingTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_symbol + assert_equal "foo", @conn.type_cast(:foo) + end + + def test_type_cast_date + date = Date.today + expected = @conn.quoted_date(date) + assert_equal expected, @conn.type_cast(date) + end + + def test_type_cast_time + time = Time.now + expected = @conn.quoted_date(time) + assert_equal expected, @conn.type_cast(time) + end + + def test_type_cast_numeric + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) + end + + def test_type_cast_nil + assert_nil @conn.type_cast(nil) + end + + def test_type_cast_unknown_should_raise_error + obj = Class.new.new + assert_raise(TypeError) { @conn.type_cast(obj) } + end + + def test_type_cast_object_which_responds_to_quoted_id + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + + def id + 10 + end + }.new + assert_equal 10, @conn.type_cast(quoted_id_obj) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + }.new + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } + end + end + class QuoteBooleanTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection @@ -161,5 +234,32 @@ module ActiveRecord assert_predicate @connection.type_cast(false), :frozen? end end + + if subsecond_precision_supported? + class QuoteARBaseTest < ActiveRecord::TestCase + class DatetimePrimaryKey < ActiveRecord::Base + end + + def setup + @time = ::Time.utc(2017, 2, 14, 12, 34, 56, 789999) + @connection = ActiveRecord::Base.connection + @connection.create_table :datetime_primary_keys, id: :datetime, precision: 3, force: true + end + + def teardown + @connection.drop_table :datetime_primary_keys, if_exists: true + end + + def test_quote_ar_object + value = DatetimePrimaryKey.new(id: @time) + assert_equal "'2017-02-14 12:34:56.789000'", @connection.quote(value) + end + + def test_type_cast_ar_object + value = DatetimePrimaryKey.new(id: @time) + assert_equal "2017-02-14 12:34:56.789000", @connection.type_cast(value) + end + end + end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index a93061b516..24b678310d 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -10,7 +10,7 @@ require "models/person" require "models/ship" class ReadOnlyTest < ActiveRecord::TestCase - fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers + fixtures :authors, :author_addresses, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers def test_cant_save_readonly_record dev = Developer.find(1) diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 5dac3d064b..c1c2efb9c8 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -86,8 +86,8 @@ class ReflectionTest < ActiveRecord::TestCase column = @first.column_for_attribute("attribute_that_doesnt_exist") assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column assert_equal "attribute_that_doesnt_exist", column.name - assert_equal nil, column.sql_type - assert_equal nil, column.type + assert_nil column.sql_type + assert_nil column.type end def test_non_existent_types_are_identity_types @@ -100,7 +100,13 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { class_name: "MyApplication::Business::Company" }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create( + :has_many, + nil, + nil, + { class_name: "MyApplication::Business::Company" }, + Customer + ) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -252,7 +258,9 @@ class ReflectionTest < ActiveRecord::TestCase [Post.reflect_on_association(:first_taggings).scope], [Author.reflect_on_association(:misc_posts).scope] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain + actual = assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain + end assert_equal expected, actual expected = [ @@ -264,7 +272,9 @@ class ReflectionTest < ActiveRecord::TestCase [], [] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + actual = assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + end assert_equal expected, actual end @@ -325,6 +335,15 @@ 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 } @@ -389,15 +408,27 @@ class ReflectionTest < ActiveRecord::TestCase end def test_through_reflection_scope_chain_does_not_modify_other_reflections - orig_conds = Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect - Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain - assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect + orig_conds = assert_deprecated do + Post.reflect_on_association(:first_blue_tags_2).scope_chain + end.inspect + assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + end + assert_equal orig_conds, assert_deprecated { + Post.reflect_on_association(:first_blue_tags_2).scope_chain + }.inspect end def test_symbol_for_class_name assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass end + def test_class_for_class_name + assert_deprecated do + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm), :validate? + end + end + def test_join_table category = Struct.new(:table_name, :pluralize_table_names).new("categories", true) product = Struct.new(:table_name, :pluralize_table_names).new("products", true) diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index d2382b9bb2..cb6e4d76d3 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -3,36 +3,15 @@ require "models/post" require "models/comment" module ActiveRecord - class DelegationTest < ActiveRecord::TestCase - fixtures :posts - - def call_method(target, method) - method_arity = target.to_a.method(method).arity - - if method_arity.zero? - target.public_send(method) - elsif method_arity < 0 - if method == :shuffle! - target.public_send(method) - else - target.public_send(method, 1) - end - elsif method_arity == 1 - target.public_send(method, 1) - else - raise NotImplementedError - end - end - end - - module DelegationWhitelistBlacklistTests + module DelegationWhitelistTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, - :to_ary, :to_set, :to_xml, :to_yaml, :join + :to_ary, :to_set, :to_xml, :to_yaml, :join, + :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json ] ARRAY_DELEGATES.each do |method| @@ -42,16 +21,18 @@ module ActiveRecord end end - class DelegationAssociationTest < DelegationTest - include DelegationWhitelistBlacklistTests + class DelegationAssociationTest < ActiveRecord::TestCase + include DelegationWhitelistTests + + fixtures :posts def target Post.first.comments end end - class DelegationRelationTest < DelegationTest - include DelegationWhitelistBlacklistTests + class DelegationRelationTest < ActiveRecord::TestCase + include DelegationWhitelistTests fixtures :comments diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 278dac8171..c3b39a9295 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -8,7 +8,7 @@ require "models/project" require "models/rating" class RelationMergingTest < ActiveRecord::TestCase - fixtures :developers, :comments, :authors, :posts + fixtures :developers, :comments, :authors, :author_addresses, :posts def test_relation_merging devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order("id ASC").where("id < 3")) @@ -21,7 +21,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_to_sql post = Post.first sql = post.comments.to_sql - assert_match(/.?post_id.? = #{post.id}\Z/i, sql) + assert_match(/.?post_id.? = #{post.id}\z/i, sql) end def test_relation_merging_with_arel_equalities_keeps_last_equality @@ -56,7 +56,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.present? + assert devs.locked? end def test_relation_merging_with_preload @@ -114,7 +114,7 @@ class RelationMergingTest < ActiveRecord::TestCase end class MergingDifferentRelationsTest < ActiveRecord::TestCase - fixtures :posts, :authors, :developers + fixtures :posts, :authors, :author_addresses, :developers test "merging where relations" do hello_by_bob = Post.where(body: "hello").joins(:author). diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 966ae83a3f..dea787c07f 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -3,7 +3,7 @@ require "models/post" module ActiveRecord class RelationMutationTest < ActiveRecord::TestCase - class FakeKlass < Struct.new(:table_name, :name) + FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache inherited self @@ -36,7 +36,7 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new("posts"), Post.arel_table, Post.predicate_builder end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") @@ -90,7 +90,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -108,7 +108,7 @@ module ActiveRecord end test "#reorder!" do - @relation = self.relation.order("foo") + @relation = relation.order("foo") assert relation.reorder!("bar").equal?(relation) assert_equal ["bar"], relation.order_values @@ -143,7 +143,7 @@ module ActiveRecord assert_equal({ foo: "bar" }, relation.create_with_value) end - test "test_merge!" do + test "merge!" do assert relation.merge!(select: :foo).equal?(relation) assert_equal [:foo], relation.select_values end @@ -161,22 +161,6 @@ module ActiveRecord test "distinct!" do relation.distinct! :foo assert_equal :foo, relation.distinct_value - - assert_deprecated do - assert_equal :foo, relation.uniq_value # deprecated access - end - end - - test "uniq! was replaced by distinct!" do - assert_deprecated(/use distinct! instead/) do - relation.uniq! :foo - end - - assert_deprecated(/use distinct_value instead/) do - assert_equal :foo, relation.uniq_value # deprecated access - end - - assert_equal :foo, relation.distinct_value end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 2796595523..abb7ca72dd 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -79,7 +79,7 @@ module ActiveRecord expected = Post.where("id = 1 or id = 2").to_a p = Post.where("id = 1") p.load - assert_equal p.loaded?, true + assert_equal true, p.loaded? assert_equal expected, p.or(Post.where("id = 2")).to_a end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index a96d1ae5b5..86e150ed79 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def test_association_not_eq - expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new)) + expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(bind_param)) relation = Post.joins(:comments).where.not(comments: { title: "hello" }) assert_equal(expected.to_sql, relation.where_clause.ast.to_sql) end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index d8e4c304f0..f8eb0dee91 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -47,15 +47,15 @@ class ActiveRecord::Relation test "merge removes bind parameters matching overlapping equality clauses" do a = WhereClause.new( [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [attribute("id", 1), attribute("name", "Sean")], + [bind_attribute("id", 1), bind_attribute("name", "Sean")], ) b = WhereClause.new( [table["name"].eq(bind_param)], - [attribute("name", "Jim")] + [bind_attribute("name", "Jim")] ) expected = WhereClause.new( [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [attribute("id", 1), attribute("name", "Jim")], + [bind_attribute("id", 1), bind_attribute("name", "Jim")], ) assert_equal expected, a.merge(b) @@ -103,10 +103,10 @@ class ActiveRecord::Relation table["name"].eq(bind_param), table["age"].gteq(bind_param), ], [ - attribute("name", "Sean"), - attribute("age", 30), + bind_attribute("name", "Sean"), + bind_attribute("age", 30), ]) - expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)]) + expected = WhereClause.new([table["age"].gteq(bind_param)], [bind_attribute("age", 30)]) assert_equal expected, where_clause.except("id", "name") end @@ -146,8 +146,8 @@ class ActiveRecord::Relation end test "or joins the two clauses using OR" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) - other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")]) + where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) + other_clause = WhereClause.new([table["name"].eq(bind_param)], [bind_attribute("name", "Sean")]) expected_ast = Arel::Nodes::Grouping.new( Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param)) @@ -159,7 +159,7 @@ class ActiveRecord::Relation end test "or returns an empty where clause when either side is empty" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) + where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) assert_equal WhereClause.empty, where_clause.or(WhereClause.empty) assert_equal WhereClause.empty, WhereClause.empty.or(where_clause) @@ -170,13 +170,5 @@ class ActiveRecord::Relation def table Arel::Table.new("table") end - - def bind_param - Arel::Nodes::BindParam.new - end - - def attribute(name, value) - ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new) - end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 925af49ffe..cbc466d6b8 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -15,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates + fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates def test_where_copies_bind_params author = authors(:david) @@ -64,12 +64,12 @@ module ActiveRecord end def test_belongs_to_array_value_where - assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql + assert_equal Post.where(author_id: [1, 2]).to_sql, Post.where(author: [1, 2]).to_sql end def test_belongs_to_nested_relation_where - expected = Post.where(author_id: Author.where(id: [1,2])).to_sql - actual = Post.where(author: Author.where(id: [1,2])).to_sql + expected = Post.where(author_id: Author.where(id: [1, 2])).to_sql + actual = Post.where(author: Author.where(id: [1, 2])).to_sql assert_equal expected, actual end @@ -87,7 +87,7 @@ module ActiveRecord def test_belongs_to_nested_where_with_relation author = authors(:david) - expected = Author.where(id: author ).joins(:posts) + expected = Author.where(id: author).joins(:posts) actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts) assert_equal expected.to_a, actual.to_a @@ -127,8 +127,8 @@ module ActiveRecord end def test_polymorphic_nested_relation_where - expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: Treasure.where(id: [1,2])) - actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) + expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: Treasure.where(id: [1, 2])) + actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1, 2])) assert_equal expected.to_sql, actual.to_sql end @@ -289,6 +289,11 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + def test_where_on_association_with_select_relation + essay = Essay.where(author: Author.where(name: "David").select(:name)).take + assert_equal essays(:david_modest_proposal), essay + end + def test_where_with_strong_parameters protected_params = Class.new do attr_reader :permitted diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 23d27ab90a..5fb32270b7 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -6,9 +6,9 @@ require "models/rating" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors + fixtures :posts, :comments, :authors, :author_addresses - class FakeKlass < Struct.new(:table_name, :name) + FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache inherited self @@ -159,7 +159,7 @@ module ActiveRecord relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation = relation.merge where: { name: :lol }, readonly: true - assert_equal({ "name"=>:lol }, relation.where_clause.to_h) + assert_equal({ "name" => :lol }, relation.where_clause.to_h) assert_equal true, relation.readonly_value end @@ -224,7 +224,7 @@ module ActiveRecord def test_relation_merging_with_merged_joins_as_symbols special_comments_with_ratings = SpecialComment.joins(:ratings) posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) - assert_equal({ 2=>1, 4=>3, 5=>1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) end def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent @@ -274,7 +274,7 @@ module ActiveRecord join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id" special_comments_with_ratings = SpecialComment.joins join_string posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) - assert_equal({ 2=>1, 4=>3, 5=>1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) end class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index dcaae5b462..81173945a3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -22,7 +22,7 @@ require "models/categorization" require "models/edge" class RelationTest < ActiveRecord::TestCase - fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, + fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :tags, :taggings, :cars, :minivans class TopicWithCallbacks < ActiveRecord::Base @@ -112,7 +112,7 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first topics = Topic.all.order("id ASC") - topics.to_a # force load + topics.load # force load assert_no_queries do assert_equal "The First Topic", topics.first.title @@ -123,7 +123,7 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first_with_limit topics = Topic.all.order("id ASC") - topics.to_a # force load + topics.load # force load assert_no_queries do assert_equal ["The First Topic", @@ -136,7 +136,7 @@ class RelationTest < ActiveRecord::TestCase def test_first_get_more_than_available topics = Topic.all.order("id ASC") unloaded_first = topics.first(10) - topics.to_a # force load + topics.load # force load assert_no_queries do loaded_first = topics.first(10) @@ -184,14 +184,14 @@ class RelationTest < ActiveRecord::TestCase def test_select_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type") - subquery = Comment.from(relation).select("type","post_count") - assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort) + subquery = Comment.from(relation).select("type", "post_count") + assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort) end def test_group_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type") subquery = Comment.from(relation).group("type").average("post_count") - assert_equal(relation.map(&:post_count).sort,subquery.values.sort) + assert_equal(relation.map(&:post_count).sort, subquery.values.sort) end def test_finding_with_conditions @@ -218,17 +218,34 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:fifth).title, topics.first.title end - def test_finding_with_reverted_assoc_order + def test_finding_with_arel_assoc_order + topics = Topic.order(Arel.sql("id") => :desc) + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + + def test_finding_with_reversed_assoc_order topics = Topic.order(id: :asc).reverse_order assert_equal 5, topics.to_a.size assert_equal topics(:fifth).title, topics.first.title end + def test_finding_with_reversed_arel_assoc_order + topics = Topic.order(Arel.sql("id") => :asc).reverse_order + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + def test_reverse_order_with_function topics = Topic.order("length(title)").reverse_order assert_equal topics(:second).title, topics.first.title end + def test_reverse_arel_assoc_order_with_function + topics = Topic.order(Arel.sql("length(title)") => :asc).reverse_order + assert_equal topics(:second).title, topics.first.title + end + def test_reverse_order_with_function_other_predicates topics = Topic.order("author_name, length(title), id").reverse_order assert_equal topics(:second).title, topics.first.title @@ -251,6 +268,12 @@ class RelationTest < ActiveRecord::TestCase end end + def test_reverse_arel_assoc_order_with_multiargument_function + assert_nothing_raised do + Topic.order(Arel.sql("REPLACE(title, '', '')") => :asc).reverse_order + end + end + def test_reverse_order_with_nulls_first_or_last assert_raises(ActiveRecord::IrreversibleOrderError) do Topic.order("title NULLS FIRST").reverse_order @@ -291,7 +314,7 @@ class RelationTest < ActiveRecord::TestCase assert_includes Topic.order(id: "DESC").to_sql, "DESC" assert_includes Topic.order(id: "desc").to_sql, "DESC" assert_includes Topic.order(id: :DESC).to_sql, "DESC" - assert_includes Topic.order(id: :desc).to_sql,"DESC" + assert_includes Topic.order(id: :desc).to_sql, "DESC" end def test_raising_exception_on_invalid_hash_params @@ -365,7 +388,7 @@ class RelationTest < ActiveRecord::TestCase end def test_finding_with_sanitized_order - query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql + query = Tag.order(["field(id, ?)", [1, 3, 2]]).to_sql assert_match(/field\(id, 1,3,2\)/, query) query = Tag.order(["field(id, ?)", []]).to_sql @@ -442,7 +465,7 @@ class RelationTest < ActiveRecord::TestCase assert_no_queries(ignore_none: false) do assert_equal 0, Developer.none.count assert_equal 0, Developer.none.calculate(:count, nil) - assert_equal nil, Developer.none.calculate(:average, "salary") + assert_nil Developer.none.calculate(:average, "salary") end end @@ -458,55 +481,55 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_sum ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:id).sum(:id) - assert_equal 0, ac.engines.count + assert_equal 0, ac.engines.count ac.save assert_equal Hash.new, ac.engines.group(:id).sum(:id) - assert_equal 0, ac.engines.count + assert_equal 0, ac.engines.count end def test_null_relation_count ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:id).count - assert_equal 0, ac.engines.count + assert_equal 0, ac.engines.count ac.save assert_equal Hash.new, ac.engines.group(:id).count - assert_equal 0, ac.engines.count + assert_equal 0, ac.engines.count end def test_null_relation_size ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:id).size - assert_equal 0, ac.engines.size + assert_equal 0, ac.engines.size ac.save assert_equal Hash.new, ac.engines.group(:id).size - assert_equal 0, ac.engines.size + assert_equal 0, ac.engines.size end def test_null_relation_average ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:car_id).average(:id) - assert_equal nil, ac.engines.average(:id) + assert_nil ac.engines.average(:id) ac.save assert_equal Hash.new, ac.engines.group(:car_id).average(:id) - assert_equal nil, ac.engines.average(:id) + assert_nil ac.engines.average(:id) end def test_null_relation_minimum ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) - assert_equal nil, ac.engines.minimum(:id) + assert_nil ac.engines.minimum(:id) ac.save assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) - assert_equal nil, ac.engines.minimum(:id) + assert_nil ac.engines.minimum(:id) end def test_null_relation_maximum ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) - assert_equal nil, ac.engines.maximum(:id) + assert_nil ac.engines.maximum(:id) ac.save assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) - assert_equal nil, ac.engines.maximum(:id) + assert_nil ac.engines.maximum(:id) end def test_null_relation_in_where_condition @@ -571,7 +594,7 @@ class RelationTest < ActiveRecord::TestCase end end - def test_respond_to_delegates_to_relation + def test_respond_to_delegates_to_arel relation = Topic.all fake_arel = Struct.new(:responds) { def respond_to?(method, access = false) @@ -584,10 +607,6 @@ class RelationTest < ActiveRecord::TestCase relation.respond_to?(:matching_attributes) assert_equal [:matching_attributes, false], fake_arel.responds.first - - fake_arel.responds = [] - relation.respond_to?(:matching_attributes, true) - assert_equal [:matching_attributes, true], fake_arel.responds.first end def test_respond_to_dynamic_finders @@ -785,7 +804,6 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id) end @@ -841,15 +859,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal author, authors.first end - class Mary < Author; end - - def test_find_by_classname - Author.create!(name: Mary.name) - assert_deprecated do - assert_equal 1, Author.where(name: Mary).size - end - end - def test_find_by_id_with_list_of_ar author = Author.first authors = Author.find_by_id([author]) @@ -979,7 +988,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(42) assert ! davids.exists?(davids.new.id) - fake = Author.where(name: "fake author") + fake = Author.where(name: "fake author") assert ! fake.exists? assert ! fake.exists?(authors(:david).id) end @@ -1014,12 +1023,6 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end - def test_destroy_all_with_conditions_is_deprecated - assert_deprecated do - assert_difference("Author.count", -1) { Author.destroy_all(name: "David") } - end - end - def test_delete_all davids = Author.where(name: "David") @@ -1027,12 +1030,6 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.loaded? end - def test_delete_all_with_conditions_is_deprecated - assert_deprecated do - assert_difference("Author.count", -1) { Author.delete_all(name: "David") } - end - end - def test_delete_all_loaded davids = Author.where(name: "David") @@ -1154,7 +1151,7 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.loaded? best_posts = posts.where(comments_count: 0) - best_posts.to_a # force load + best_posts.load # force load assert_no_queries { assert_equal 9, best_posts.size } end @@ -1165,7 +1162,7 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.loaded? best_posts = posts.where(comments_count: 0) - best_posts.to_a # force load + best_posts.load # force load assert_no_queries { assert_equal 9, best_posts.size } end @@ -1175,7 +1172,7 @@ class RelationTest < ActiveRecord::TestCase assert_no_queries { assert_equal 0, posts.size } assert ! posts.loaded? - posts.to_a # force load + posts.load # force load assert_no_queries { assert_equal 0, posts.size } end @@ -1204,7 +1201,7 @@ class RelationTest < ActiveRecord::TestCase assert ! no_posts.loaded? best_posts = posts.where(comments_count: 0) - best_posts.to_a # force load + best_posts.load # force load assert_no_queries { assert_equal false, best_posts.empty? } end @@ -1522,7 +1519,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.where(author_id: 1).to_a, author_posts.to_a all_posts = relation.only(:limit) - assert_equal Post.limit(1).to_a.first, all_posts.first + assert_equal Post.limit(1).to_a, all_posts.to_a end def test_anonymous_extension @@ -1639,9 +1636,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal "David", topic2.reload.author_name end - def test_update_on_relation_passing_active_record_object_is_deprecated + def test_update_on_relation_passing_active_record_object_is_not_permitted topic = Topic.create!(title: "Foo", author_name: nil) - assert_deprecated(/update/) do + assert_raises(ArgumentError) do Topic.where(id: topic.id).update(topic, title: "Bar") end end @@ -1655,17 +1652,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal ["Foo", "Foo"], query.map(&:name) assert_sql(/DISTINCT/) do assert_equal ["Foo"], query.distinct.map(&:name) - assert_deprecated { assert_equal ["Foo"], query.uniq.map(&:name) } end assert_sql(/DISTINCT/) do assert_equal ["Foo"], query.distinct(true).map(&:name) - assert_deprecated { assert_equal ["Foo"], query.uniq(true).map(&:name) } end assert_equal ["Foo", "Foo"], query.distinct(true).distinct(false).map(&:name) - - assert_deprecated do - assert_equal ["Foo", "Foo"], query.uniq(true).uniq(false).map(&:name) - end end def test_doesnt_add_having_values_if_options_are_blank @@ -1815,7 +1806,7 @@ class RelationTest < ActiveRecord::TestCase end test "find_by returns nil if the record is missing" do - assert_equal nil, Post.all.find_by("1 = 0") + assert_nil Post.all.find_by("1 = 0") end test "find_by doesn't have implicit ordering" do @@ -1906,6 +1897,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal "#<ActiveRecord::Relation [#{Post.limit(10).map(&:inspect).join(', ')}, ...]>", relation.inspect end + test "relations don't load all records in #inspect" do + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + Post.all.inspect + end + end + test "already-loaded relations don't perform a new query in #inspect" do relation = Post.limit(2) relation.to_a @@ -1963,25 +1960,49 @@ class RelationTest < ActiveRecord::TestCase assert !Post.all.respond_to?(:by_lifo) end + def test_unscope_with_subquery + p1 = Post.where(id: 1) + p2 = Post.where(id: 2) + + assert_not_equal p1, p2 + + comments = Comment.where(post: p1).unscope(where: :post_id).where(post: p2) + + assert_not_equal p1.first.comments, comments + assert_equal p2.first.comments, comments + end + + def test_unscope_specific_where_value + posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day") + + assert_equal 1, posts.count + assert_equal 1, posts.unscope(where: :title).count + assert_equal 1, posts.unscope(where: :body).count + end + def test_unscope_removes_binds - left = Post.where(id: Arel::Nodes::BindParam.new) - column = Post.columns_hash["id"] - left.bind_values += [[column, 20]] + left = Post.where(id: 20) + + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] + assert_equal binds, left.bound_attributes relation = left.unscope(where: :id) - assert_equal [], relation.bind_values + assert_equal [], relation.bound_attributes end - def test_merging_removes_rhs_bind_parameters - left = Post.where(id: 20) - right = Post.where(id: [1,2,3,4]) + def test_merging_removes_rhs_binds + left = Post.where(id: 20) + right = Post.where(id: [1, 2, 3, 4]) + + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] + assert_equal binds, left.bound_attributes merged = left.merge(right) - assert_equal [], merged.bind_values + assert_equal [], merged.bound_attributes end - def test_merging_keeps_lhs_bind_parameters - binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))] + def test_merging_keeps_lhs_binds + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] right = Post.where(id: 20) left = Post.where(id: 10) @@ -1990,15 +2011,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal binds, merged.bound_attributes end - def test_merging_reorders_bind_params - post = Post.first - right = Post.where(id: post.id) - left = Post.where(title: post.title) - - merged = left.merge(right) - assert_equal post, merged.first - end - def test_relation_join_method assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 5dc9d6d8b7..3f4c0c03e3 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -13,7 +13,7 @@ class ReloadModelsTest < ActiveRecord::TestCase # development environment. Note that meanwhile the class Pet is not # reloaded, simulating a class that is present in a plugin. Object.class_eval { remove_const :Owner } - Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb"))) + Kernel.load(File.expand_path("../models/owner.rb", __dir__)) pet = Pet.find_by_name("parrot") pet.owner = Owner.find_by_name("ashley") diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 949086fda0..1a0b7c6ca7 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -45,10 +45,8 @@ module ActiveRecord end end - if Enumerator.method_defined? :size - test "each without block returns a sized enumerator" do - assert_equal 3, result.each.size - end + test "each without block returns a sized enumerator" do + assert_equal 3, result.each.size end test "cast_values returns rows after type casting" do diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 464bb12ccb..72f09186e2 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -12,8 +12,8 @@ class SanitizeTest < ActiveRecord::TestCase 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]) 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.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]) end def test_sanitize_sql_array_handles_bind_variables @@ -152,11 +152,15 @@ class SanitizeTest < ActiveRecord::TestCase end def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal "1", bind("?", o) + o = Class.new { + def quoted_id + 1 + end + }.new + assert_deprecated { assert_equal "1", bind("?", o) } os = [o] * 3 - assert_equal "1,1,1", bind("?", os) + assert_deprecated { assert_equal "1,1,1", bind("?", os) } end def test_named_bind_with_postgresql_type_casts diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 57b1bc889a..cb8d449ba9 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -51,6 +51,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output + assert_no_match %r{(?<=, ) do \|t\|}, output assert_no_match %r{create_table "schema_migrations"}, output assert_no_match %r{create_table "ar_internal_metadata"}, output end @@ -147,12 +148,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*limit: 4}, output end - if current_adapter?(:SQLite3Adapter) - assert_match %r{c_int_5.*limit: 5}, output - assert_match %r{c_int_6.*limit: 6}, output - assert_match %r{c_int_7.*limit: 7}, output - assert_match %r{c_int_8.*limit: 8}, output - elsif current_adapter?(:OracleAdapter) + if current_adapter?(:SQLite3Adapter, :OracleAdapter) assert_match %r{c_int_5.*limit: 5}, output assert_match %r{c_int_6.*limit: 6}, output assert_match %r{c_int_7.*limit: 7}, output @@ -182,22 +178,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_columns_in_right_order - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip + if current_adapter?(:PostgreSQLAdapter) + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition + elsif 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 else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip - if current_adapter?(:PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:Mysql2Adapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition - elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip + if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition else assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end @@ -250,9 +248,9 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_type - output = standard_dump - assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + output = dump_table_schema "key_tests" + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext$}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza"$}, output end end @@ -263,23 +261,34 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) def test_schema_dump_includes_bigint_default - output = standard_dump + output = dump_table_schema "defaults" assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output end def test_schema_dump_includes_limit_on_array_type - output = standard_dump + output = dump_table_schema "bigint_array" assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output end def test_schema_dump_allows_array_of_decimal_defaults - output = standard_dump + output = dump_table_schema "bigint_array" assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output end def test_schema_dump_expression_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_equal 't.index "lower((name)::text)", name: "company_expression_index", using: :btree', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition + end + + def test_schema_dump_interval_type + output = dump_table_schema "postgresql_times" + assert_match %r{t\.interval\s+"time_interval"$}, output + assert_match %r{t\.interval\s+"scaled_time_interval",\s+precision: 6$}, output + end + + def test_schema_dump_oid_type + output = dump_table_schema "postgresql_oids" + assert_match %r{t\.oid\s+"obj_id"$}, output end if ActiveRecord::Base.connection.supports_extensions? @@ -343,9 +352,9 @@ class SchemaDumperTest < ActiveRecord::TestCase create_table("dogs") do |t| t.column :name, :string - t.column :owner_id, :integer + t.references :owner t.index [:name] - t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? + t.foreign_key :dog_owners, column: "owner_id" end end def down @@ -417,25 +426,40 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection - @connection.create_table :defaults, force: true do |t| + @connection.create_table :dump_defaults, force: true do |t| t.string :string_with_default, default: "Hello!" t.date :date_with_default, default: "2014-06-05" t.datetime :datetime_with_default, default: "2014-06-05 07:17:04" t.time :time_with_default, default: "07:17:04" + t.decimal :decimal_with_default, default: "1234567890.0123456789", precision: 20, scale: 10 + end + + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table :infinity_defaults, force: true do |t| + t.float :float_with_inf_default, default: Float::INFINITY + t.float :float_with_nan_default, default: Float::NAN + end end end teardown do - return unless @connection - @connection.drop_table "defaults", if_exists: true + @connection.drop_table "dump_defaults", if_exists: true end def test_schema_dump_defaults_with_universally_supported_types - output = dump_table_schema("defaults") + output = dump_table_schema("dump_defaults") assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output - assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output - assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output - assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output + assert_match %r{t\.date\s+"date_with_default",\s+default: "2014-06-05"}, output + assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: "2014-06-05 07:17:04"}, output + assert_match %r{t\.time\s+"time_with_default",\s+default: "2000-01-01 07:17:04"}, output + assert_match %r{t\.decimal\s+"decimal_with_default",\s+precision: 20,\s+scale: 10,\s+default: "1234567890.0123456789"}, output + end + + def test_schema_dump_with_float_column_infinity_default + skip unless current_adapter?(:PostgreSQLAdapter) + output = dump_table_schema("infinity_defaults") + assert_match %r{t\.float\s+"float_with_inf_default",\s+default: ::Float::INFINITY}, output + assert_match %r{t\.float\s+"float_with_nan_default",\s+default: ::Float::NAN}, output end end diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb index 3d92a5e104..362370ac61 100644 --- a/activerecord/test/cases/schema_loading_test.rb +++ b/activerecord/test/cases/schema_loading_test.rb @@ -8,7 +8,7 @@ module SchemaLoadCounter def load_schema! self.load_schema_calls ||= 0 - self.load_schema_calls +=1 + self.load_schema_calls += 1 super end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 61062da3e1..89fb434b27 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -5,7 +5,7 @@ require "models/developer" require "models/computer" require "models/vehicle" require "models/cat" -require "active_support/core_ext/regexp" +require "concurrent/atomic/cyclic_barrier" class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -51,7 +51,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scope_with_conditions_string assert_equal Developer.where(name: "David").map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort - assert_equal nil, DeveloperCalledDavid.create!.name + assert_nil DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash @@ -59,17 +59,6 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal "Jamis", DeveloperCalledJamis.create!.name end - unless in_memory_db? - def test_default_scoping_with_threads - 2.times do - Thread.new { - assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" - DeveloperOrderedBySalary.connection.close - }.join - end - end - end - def test_default_scope_with_inheritance wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres["name"] @@ -315,7 +304,7 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_create_attribute_overwrites_default_values - assert_equal nil, PoorDeveloperCalledJamis.create!(salary: nil).salary + assert_nil PoorDeveloperCalledJamis.create!(salary: nil).salary assert_equal 50000, PoorDeveloperCalledJamis.create!(name: "David").salary end @@ -433,24 +422,6 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id) end - unless in_memory_db? - def test_default_scope_is_threadsafe - threads = [] - assert_not_equal 1, ThreadsafeDeveloper.unscoped.count - - threads << Thread.new do - Thread.current[:long_default_scope] = true - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads << Thread.new do - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads.each(&:join) - end - end - test "additional conditions are ANDed with the default scope" do scope = DeveloperCalledJamis.where(name: "David") assert_equal 2, scope.where_clause.ast.children.length @@ -491,6 +462,8 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_with_abstract_class_scope_should_be_executed_in_correct_context vegetarian_pattern, gender_pattern = if current_adapter?(:Mysql2Adapter) [/`lions`.`is_vegetarian`/, /`lions`.`gender`/] + elsif current_adapter?(:OracleAdapter) + [/"LIONS"."IS_VEGETARIAN"/, /"LIONS"."GENDER"/] else [/"lions"."is_vegetarian"/, /"lions"."gender"/] end @@ -499,3 +472,37 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_match gender_pattern, Lion.female.to_sql end end + +class DefaultScopingWithThreadTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def test_default_scoping_with_threads + 2.times do + Thread.new { + assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" + DeveloperOrderedBySalary.connection.close + }.join + end + end + + def test_default_scope_is_threadsafe + threads = [] + assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + + barrier_1 = Concurrent::CyclicBarrier.new(2) + barrier_2 = Concurrent::CyclicBarrier.new(2) + + threads << Thread.new do + Thread.current[:default_scope_delay] = -> { barrier_1.wait; barrier_2.wait } + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads << Thread.new do + Thread.current[:default_scope_delay] = -> { barrier_2.wait } + barrier_1.wait + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads.each(&:join) + end +end unless in_memory_db? diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 58e1310ab0..483ea7128d 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -23,8 +23,8 @@ class NamedScopingTest < ActiveRecord::TestCase all_posts = Topic.base assert_queries(1) do - all_posts.collect - all_posts.collect + all_posts.collect { true } + all_posts.collect { true } end end @@ -119,8 +119,8 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scope_with_STI - assert_equal 3,Post.containing_the_letter_a.count - assert_equal 1,SpecialPost.containing_the_letter_a.count + assert_equal 3, Post.containing_the_letter_a.count + assert_equal 1, SpecialPost.containing_the_letter_a.count end def test_has_many_through_associations_have_access_to_scopes @@ -161,13 +161,13 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_first_and_last_should_allow_integers_for_limit - assert_equal Topic.base.first(2), Topic.base.to_a.first(2) + assert_equal Topic.base.first(2), Topic.base.order("id").to_a.first(2) assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2) end def test_first_and_last_should_not_use_query_when_results_are_loaded topics = Topic.base - topics.reload # force load + topics.load # force load assert_no_queries do topics.first topics.last @@ -178,7 +178,7 @@ class NamedScopingTest < ActiveRecord::TestCase topics = Topic.base assert_queries(2) do topics.empty? # use count query - topics.collect # force load + topics.load # force load topics.empty? # use loaded (no query) end end @@ -187,7 +187,7 @@ class NamedScopingTest < ActiveRecord::TestCase topics = Topic.base assert_queries(2) do topics.any? # use count query - topics.collect # force load + topics.load # force load topics.any? # use loaded (no query) end end @@ -203,7 +203,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_any_should_not_fire_query_if_scope_loaded topics = Topic.base - topics.collect # force load + topics.load # force load assert_no_queries { assert topics.any? } end @@ -217,7 +217,7 @@ class NamedScopingTest < ActiveRecord::TestCase topics = Topic.base assert_queries(2) do topics.many? # use count query - topics.collect # force load + topics.load # force load topics.many? # use loaded (no query) end end @@ -233,7 +233,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_not_fire_query_if_scope_loaded topics = Topic.base - topics.collect # force load + topics.load # force load assert_no_queries { assert topics.many? } end @@ -384,7 +384,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_size_should_use_length_when_results_are_loaded topics = Topic.base - topics.reload # force load + topics.load # force load assert_no_queries do topics.size # use loaded (no query) end @@ -551,6 +551,12 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 1, SpecialComment.where(body: "go crazy").created.count end + def test_model_class_should_respond_to_extending + assert_raises OopsError do + Comment.unscoped.oops_comments.destroy_all + end + end + def test_model_class_should_respond_to_none assert !Topic.none? Topic.delete_all diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 27b4583457..8535be8402 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -10,7 +10,7 @@ require "models/person" require "models/reference" class RelationScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + fixtures :authors, :author_addresses, :developers, :projects, :comments, :posts, :developers_projects setup do developers(:david) @@ -28,7 +28,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scope_breaks_caching_on_collections author = authors :david ids = author.reload.special_posts_with_default_scope.map(&:id) - assert_equal [1,5,6], ids.sort + assert_equal [1, 5, 6], ids.sort scoped_posts = SpecialPostWithDefaultScope.unscoped do author = authors :david author.reload.special_posts_with_default_scope.to_a @@ -229,16 +229,23 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_circular_joins_with_current_scope_does_not_crash + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do - Post.current_scope.first(10) + Post.first(10) end assert_equal posts, Post.joins(comments: :post).first(10) end + + def test_circular_left_joins_with_scoping_does_not_crash + posts = Post.left_joins(comments: :post).scoping do + Post.first(10) + end + assert_equal posts, Post.left_joins(comments: :post).first(10) + end end class NestedRelationScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts + fixtures :authors, :author_addresses, :developers, :projects, :comments, :posts def test_merge_options Developer.where("salary = 80000").scoping do @@ -277,7 +284,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase assert_equal "David", Developer.first.name Developer.unscoped.where("name = 'Maiha'") do - assert_equal nil, Developer.first + assert_nil Developer.first end # ensure that scoping is restored diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb index eda0229c26..7b9cbee40a 100644 --- a/activerecord/test/cases/secure_token_test.rb +++ b/activerecord/test/cases/secure_token_test.rb @@ -27,6 +27,6 @@ class SecureTokenTest < ActiveRecord::TestCase @user.token = "custom-secure-token" @user.save - assert_equal @user.token, "custom-secure-token" + assert_equal "custom-secure-token", @user.token end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index bebd856faf..e1bdaab5cf 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -107,7 +107,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialized_time_attribute - myobj = Time.local(2008,1,1,1,0) + myobj = Time.local(2008, 1, 1, 1, 0) topic = Topic.create("content" => myobj).reload assert_equal(myobj, topic.content) end @@ -185,14 +185,14 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.new(content: true) assert topic.save topic = topic.reload - assert_equal topic.content, true + assert_equal true, topic.content end def test_serialized_boolean_value_false topic = Topic.new(content: false) assert topic.save topic = topic.reload - assert_equal topic.content, false + assert_equal false, topic.content end def test_serialize_with_coder @@ -211,7 +211,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.save! topic.reload assert_kind_of some_class, topic.content - assert_equal topic.content, some_class.new("my value") + assert_equal some_class.new("my value"), topic.content end def test_serialize_attribute_via_select_method_when_time_zone_available @@ -240,6 +240,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal [], light.long_state end + def test_unexpected_serialized_type + Topic.serialize :content, Hash + topic = Topic.create!(content: { zomg: true }) + + Topic.serialize :content, Array + + topic.reload + error = assert_raise(ActiveRecord::SerializationTypeMismatch) do + topic.content + end + expected = "can't load `content`: was supposed to be a Array, but was a Hash. -- {:zomg=>true}" + assert_equal expected, error.to_s + end + def test_serialized_column_should_unserialize_after_update_column t = Topic.create(content: "first") assert_equal("first", t.content) @@ -335,4 +349,32 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.foo refute topic.changed? end + + def test_serialized_attribute_works_under_concurrent_initial_access + model = Topic.dup + + topic = model.last + topic.update group: "1" + + model.serialize :group, JSON + model.reset_column_information + + # This isn't strictly necessary for the test, but a little bit of + # knowledge of internals allows us to make failures far more likely. + model.define_singleton_method(:define_attribute) do |*args| + Thread.pass + super(*args) + end + + threads = 4.times.map do + Thread.new do + topic.reload.group + end + end + + # All the threads should retrieve the value knowing it is JSON, and + # thus decode it. If this fails, some threads will instead see the + # raw string ("1"), or raise an exception. + assert_equal [1] * threads.size, threads.map(&:value) + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index f45f63c68e..fab3648564 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -105,5 +105,31 @@ module ActiveRecord refute_equal book, other_book end + + def test_find_by_does_not_use_statement_cache_if_table_name_is_changed + book = Book.create(name: "my book") + + Book.find_by(name: book.name) # warming the statement cache. + + # changing the table name should change the query that is not cached. + Book.table_name = :birds + assert_nil Book.find_by(name: book.name) + ensure + Book.table_name = :books + end + + def test_find_does_not_use_statement_cache_if_table_name_is_changed + book = Book.create(name: "my book") + + Book.find(book.id) # warming the statement cache. + + # changing the table name should change the query that is not cached. + Book.table_name = :birds + assert_raise ActiveRecord::RecordNotFound do + Book.find(book.id) + end + ensure + Book.table_name = :books + end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index d847a02679..c47c97e9d9 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -24,7 +24,7 @@ module ActiveRecord sqlite3: :sqlite_tasks } - class DatabaseTasksUtilsTask< ActiveRecord::TestCase + class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment ActiveRecord::Migrator.stubs(:current_version).returns(1) @@ -61,7 +61,7 @@ module ActiveRecord instance = klazz.new klazz.stubs(:new).returns instance - instance.expects(:structure_dump).with("awesome-file.sql") + instance.expects(:structure_dump).with("awesome-file.sql", nil) ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql") @@ -85,11 +85,23 @@ module ActiveRecord end end + class DatabaseTasksDumpSchemaCacheTest < ActiveRecord::TestCase + def test_dump_schema_cache + path = "/tmp/my_schema_cache.yml" + ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path) + assert File.file?(path) + ensure + FileUtils.rm_rf(path) + end + end + class DatabaseTasksCreateAllTest < ActiveRecord::TestCase def setup @configurations = { "development" => { "database" => "my-db" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) + # To refrain from connecting to a newly created empty DB in sqlite3_mem tests + ActiveRecord::Base.connection_handler.stubs(:establish_connection) end def test_ignores_configurations_without_databases @@ -331,13 +343,37 @@ module ActiveRecord 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("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 + + ENV["VERBOSE"] = "yes" + ENV["VERSION"] = "unknown" + 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 end + def test_migrate_raise_error_on_empty_version + version = ENV["VERSION"] + ENV["VERSION"] = "" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_equal "Empty VERSION provided", e.message + ensure + ENV["VERSION"] = version + end + def test_migrate_clears_schema_cache_afterward ActiveRecord::Base.expects(:clear_cache!) ActiveRecord::Tasks::DatabaseTasks.migrate @@ -411,7 +447,7 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_dump") do - eval("@#{v}").expects(:structure_dump).with("awesome-file.sql") + eval("@#{v}").expects(:structure_dump).with("awesome-file.sql", nil) ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql") end end @@ -422,7 +458,7 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_load") do - eval("@#{v}").expects(:structure_load).with("awesome-file.sql") + eval("@#{v}").expects(:structure_load).with("awesome-file.sql", nil) ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql") end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index dbe935808e..c22d974536 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -59,7 +59,7 @@ if current_adapter?(:Mysql2Adapter) def test_when_database_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stdout.string, "Created database 'my-app-db'\n" + assert_equal "Created database 'my-app-db'\n", $stdout.string end def test_create_when_database_exists_outputs_info_to_stderr @@ -69,7 +69,7 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stderr.string, "Database 'my-app-db' already exists\n" + assert_equal "Database 'my-app-db' already exists\n", $stderr.string end end @@ -167,7 +167,7 @@ if current_adapter?(:Mysql2Adapter) def assert_permissions_granted_for(db_user) db_name = @configuration["database"] db_password = @configuration["password"] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") end end @@ -205,7 +205,7 @@ if current_adapter?(:Mysql2Adapter) def test_when_database_dropped_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + assert_equal "Dropped database 'my-app-db'\n", $stdout.string end end @@ -294,6 +294,26 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end + def test_structure_dump_with_extra_flags + filename = "awesome-file.sql" + expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"] + + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_dump_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + end + end + + def test_structure_dump_with_ignore_tables + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" Kernel.expects(:system) @@ -323,6 +343,15 @@ if current_adapter?(:Mysql2Adapter) @configuration.merge("sslca" => "ca.crt"), filename) end + + private + def with_structure_dump_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags + ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = old + end end class MySQLStructureLoadTest < ActiveRecord::TestCase @@ -335,11 +364,23 @@ if current_adapter?(:Mysql2Adapter) def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") - .returns(true) + expected_command = ["mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db", "--noop"] - ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_load_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end end + + private + def with_structure_load_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_load_flags + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = old + end end end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index b8c8ec88f0..a2e968aedf 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -74,7 +74,7 @@ if current_adapter?(:PostgreSQLAdapter) def test_when_database_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stdout.string, "Created database 'my-app-db'\n" + assert_equal "Created database 'my-app-db'\n", $stdout.string end def test_create_when_database_exists_outputs_info_to_stderr @@ -84,7 +84,7 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stderr.string, "Database 'my-app-db' already exists\n" + assert_equal "Database 'my-app-db' already exists\n", $stderr.string end end @@ -126,7 +126,7 @@ if current_adapter?(:PostgreSQLAdapter) def test_when_database_dropped_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + assert_equal "Dropped database 'my-app-db'\n", $stdout.string end end @@ -217,17 +217,21 @@ if current_adapter?(:PostgreSQLAdapter) class PostgreSQLStructureDumpTest < ActiveRecord::TestCase def setup - @connection = stub(structure_dump: true) + @connection = stub(schema_search_path: nil, structure_dump: true) @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - @filename = "awesome-file.sql" + @filename = "/tmp/awesome-file.sql" + FileUtils.touch(@filename) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) Kernel.stubs(:system) - File.stubs(:open) + end + + def teardown + FileUtils.rm_f(@filename) end def test_structure_dump @@ -236,6 +240,33 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end + def test_structure_dump_header_comments_removed + Kernel.stubs(:system).returns(true) + File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + + assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2) + end + + def test_structure_dump_with_extra_flags + expected_command = ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--noop", "my-app-db"] + + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_dump_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + end + end + + def test_structure_dump_with_ignore_tables + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + def test_structure_dump_with_schema_search_path @configuration["schema_search_path"] = "foo,bar" @@ -263,7 +294,6 @@ if current_adapter?(:PostgreSQLAdapter) end private - def with_dump_schemas(value, &block) old_dump_schemas = ActiveRecord::Base.dump_schemas ActiveRecord::Base.dump_schemas = value @@ -271,6 +301,14 @@ if current_adapter?(:PostgreSQLAdapter) ensure ActiveRecord::Base.dump_schemas = old_dump_schemas end + + def with_structure_dump_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags + ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = old + end end class PostgreSQLStructureLoadTest < ActiveRecord::TestCase @@ -293,12 +331,32 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end + def test_structure_load_with_extra_flags + filename = "awesome-file.sql" + expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]] + + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_load_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end + end + def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end + + private + def with_structure_load_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_load_flags + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = old + end end end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 141048bfe7..ccb3834fee 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -34,7 +34,7 @@ if current_adapter?(:SQLite3Adapter) def test_when_db_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal $stdout.string, "Created database '#{@database}'\n" + assert_equal "Created database '#{@database}'\n", $stdout.string end def test_db_create_when_file_exists @@ -42,7 +42,7 @@ if current_adapter?(:SQLite3Adapter) ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal $stderr.string, "Database '#{@database}' already exists\n" + assert_equal "Database '#{@database}' already exists\n", $stderr.string end def test_db_create_with_file_does_nothing @@ -128,7 +128,7 @@ if current_adapter?(:SQLite3Adapter) def test_when_db_dropped_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" - assert_equal $stdout.string, "Dropped database '#{@database}'\n" + assert_equal "Dropped database '#{@database}'\n", $stdout.string end end @@ -180,6 +180,9 @@ if current_adapter?(:SQLite3Adapter) "adapter" => "sqlite3", "database" => @database } + + `sqlite3 #{@database} 'CREATE TABLE bar(id INTEGER)'` + `sqlite3 #{@database} 'CREATE TABLE foo(id INTEGER)'` end def test_structure_dump @@ -189,6 +192,23 @@ if current_adapter?(:SQLite3Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, "/rails/root" assert File.exist?(dbfile) assert File.exist?(filename) + assert_match(/CREATE TABLE foo/, File.read(filename)) + assert_match(/CREATE TABLE bar/, File.read(filename)) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + + def test_structure_dump_with_ignore_tables + dbfile = @database + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"]) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") + assert File.exist?(dbfile) + assert File.exist?(filename) + assert_match(/bar/, File.read(filename)) + assert_no_match(/foo/, File.read(filename)) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 60ac3e08a1..9f594fef85 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -2,7 +2,6 @@ require "active_support/test_case" require "active_support/testing/autorun" require "active_support/testing/method_call_assertions" require "active_support/testing/stream" -require "active_support/core_ext/regexp" require "active_record/fixtures" require "cases/validations_repair_helper" @@ -64,11 +63,11 @@ module ActiveRecord assert_queries(0, options, &block) end - def assert_column(model, column_name, msg=nil) + def assert_column(model, column_name, msg = nil) assert has_column?(model, column_name), msg end - def assert_no_column(model, column_name, msg=nil) + def assert_no_column(model, column_name, msg = nil) assert_not has_column?(model, column_name), msg end @@ -76,6 +75,14 @@ module ActiveRecord model.reset_column_information model.column_names.include?(column_name.to_s) end + + def bind_param + Arel::Nodes::BindParam.new + end + + def bind_attribute(name, value, type = ActiveRecord::Type.default_value) + ActiveRecord::Relation::QueryAttribute.new(name, value, type) + end end class PostgreSQLTestCase < TestCase @@ -125,12 +132,9 @@ module ActiveRecord end def call(name, start, finish, message_id, values) - sql = values[:sql] - - # FIXME: this seems bad. we should probably have a better way to indicate - # the query was cached - return if "CACHE" == values[:name] + return if values[:cached] + sql = values[:sql] self.class.log_all << sql self.class.log << sql unless ignore.match?(sql) end diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index 14a5faa85e..58d3bea3a2 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -3,28 +3,10 @@ require "cases/helper" class TestFixturesTest < ActiveRecord::TestCase setup do @klass = Class.new - @klass.send(:include, ActiveRecord::TestFixtures) - end - - def test_deprecated_use_transactional_fixtures= - assert_deprecated "use use_transactional_tests= instead" do - @klass.use_transactional_fixtures = true - end - end - - def test_use_transactional_tests_prefers_use_transactional_fixtures - ActiveSupport::Deprecation.silence do - @klass.use_transactional_fixtures = false - end - - assert_equal false, @klass.use_transactional_tests + @klass.include(ActiveRecord::TestFixtures) end def test_use_transactional_tests_defaults_to_true - ActiveSupport::Deprecation.silence do - @klass.use_transactional_fixtures = nil - end - assert_equal true, @klass.use_transactional_tests end diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 03f6c234e8..09c585167e 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -68,7 +68,7 @@ if subsecond_precision_supported? assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_time_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index cd83518e84..39b40e3411 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -422,7 +422,7 @@ class TimestampTest < ActiveRecord::TestCase self.table_name = "people" before_create do - self.born_at = self.created_at + self.born_at = created_at end end @@ -430,34 +430,19 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal person.born_at, nil end - def test_timestamp_attributes_for_create - toy = Toy.first - assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create) - end - - def test_timestamp_attributes_for_update - toy = Toy.first - assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update) - end - - def test_all_timestamp_attributes - toy = Toy.first - assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes) - end - def test_timestamp_attributes_for_create_in_model toy = Toy.first - assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model) + assert_equal ["created_at"], toy.send(:timestamp_attributes_for_create_in_model) end def test_timestamp_attributes_for_update_in_model toy = Toy.first - assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model) + assert_equal ["updated_at"], toy.send(:timestamp_attributes_for_update_in_model) end def test_all_timestamp_attributes_in_model toy = Toy.first - assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) + assert_equal ["created_at", "updated_at"], toy.send(:all_timestamp_attributes_in_model) end def test_index_is_created_for_both_timestamps diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index bd50fe55e9..eaa4dd09a9 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -243,14 +243,14 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint - def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + def @first.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i = 0); @commits ||= 0; @commits += i if i; end @first.after_rollback_block { |r| r.rollbacks(1) } @first.after_commit_block { |r| r.commits(1) } second = TopicWithCallbacks.find(3) - def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def second.commits(i=0); @commits ||= 0; @commits += i if i; end + def second.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end + def second.commits(i = 0); @commits ||= 0; @commits += i if i; end second.after_rollback_block { |r| r.rollbacks(1) } second.after_commit_block { |r| r.commits(1) } @@ -269,8 +269,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails - def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + def @first.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i = 0); @commits ||= 0; @commits += i if i; end @first.after_rollback_block { |r| r.rollbacks(1) } @first.after_commit_block { |r| r.commits(1) } @@ -449,6 +449,51 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase end end +class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase + class TopicWithHistory < ActiveRecord::Base + self.table_name = :topics + + def self.clear_history + @@history = [] + end + + def self.history + @@history ||= [] + end + end + + class TopicWithCallbacksOnDestroy < TopicWithHistory + after_commit(on: :destroy) { |record| record.class.history << :destroy } + end + + class TopicWithCallbacksOnUpdate < TopicWithHistory + after_commit(on: :update) { |record| record.class.history << :update } + end + + def test_trigger_once_on_multiple_deletions + TopicWithCallbacksOnDestroy.clear_history + topic = TopicWithCallbacksOnDestroy.new + topic.save + topic_clone = TopicWithCallbacksOnDestroy.find(topic.id) + topic.destroy + topic_clone.destroy + + assert_equal [:destroy], TopicWithCallbacksOnDestroy.history + end + + def test_trigger_on_update_where_row_was_deleted + TopicWithCallbacksOnUpdate.clear_history + topic = TopicWithCallbacksOnUpdate.new + topic.save + topic_clone = TopicWithCallbacksOnUpdate.find(topic.id) + topic.destroy + topic_clone.author_name = "Test Author" + topic_clone.save + + assert_equal [], TopicWithCallbacksOnUpdate.history + end +end + class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base self.table_name = :topics @@ -506,3 +551,43 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:rollback], @topic.history end end + +class CallbacksOnActionAndConditionTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class TopicWithCallbacksOnActionAndCondition < ActiveRecord::Base + self.table_name = :topics + + after_commit(on: [:create, :update], if: :run_callback?) { |record| record.history << :create_or_update } + + def clear_history + @history = [] + end + + def history + @history ||= [] + end + + def run_callback? + self.history << :run_callback? + true + end + + attr_accessor :save_before_commit_history, :update_title + end + + def test_callback_on_action_with_condition + topic = TopicWithCallbacksOnActionAndCondition.new + topic.save + assert_equal [:run_callback?, :create_or_update], topic.history + + topic.clear_history + topic.approved = true + topic.save + assert_equal [:run_callback?, :create_or_update], topic.history + + topic.clear_history + topic.destroy + assert_equal [], topic.history + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 834365660f..5c6d78b574 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -10,7 +10,7 @@ require "models/movie" class TransactionTest < ActiveRecord::TestCase self.use_transactional_tests = false - fixtures :topics, :developers, :authors, :posts + fixtures :topics, :developers, :authors, :author_addresses, :posts def setup @first, @second = Topic.find(1, 2).sort_by(&:id) @@ -98,7 +98,7 @@ class TransactionTest < ActiveRecord::TestCase end Topic.transaction do - @first.approved = true + @first.approved = true @first.save! end @@ -160,7 +160,7 @@ class TransactionTest < ActiveRecord::TestCase assert !@first.approved Topic.transaction do - @first.approved = true + @first.approved = true @first.save! end assert !Topic.find(@first.id).approved?, "Should not commit the approved flag" @@ -206,16 +206,6 @@ class TransactionTest < ActiveRecord::TestCase assert_equal posts_count, author.posts.reload.size end - def test_cancellation_from_returning_false_in_before_filter - def @first.before_save_for_transaction - false - end - - assert_deprecated do - @first.save - end - end - def test_cancellation_from_before_destroy_rollbacks_in_destroy add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count @@ -279,7 +269,11 @@ class TransactionTest < ActiveRecord::TestCase e = assert_raises(RuntimeError) { new_topic.save } assert_equal "Make the transaction rollback", e.message assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" - assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + if id_snapshot.nil? + assert_nil new_topic.id, "The topic should have its old id" + else + assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + end assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) end end @@ -443,16 +437,16 @@ class TransactionTest < ActiveRecord::TestCase def test_using_named_savepoints Topic.transaction do - @first.approved = true + @first.approved = true @first.save! Topic.connection.create_savepoint("first") - @first.approved = false + @first.approved = false @first.save! Topic.connection.rollback_to_savepoint("first") assert @first.reload.approved? - @first.approved = false + @first.approved = false @first.save! Topic.connection.release_savepoint("first") assert_not @first.reload.approved? diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb index bc4900e1c2..6848619ece 100644 --- a/activerecord/test/cases/type/date_time_test.rb +++ b/activerecord/test/cases/type/date_time_test.rb @@ -3,7 +3,7 @@ require "models/task" module ActiveRecord module Type - class IntegerTest < ActiveRecord::TestCase + class DateTimeTest < ActiveRecord::TestCase def test_datetime_seconds_precision_applied_to_timestamp skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported? p = Task.create!(starting: ::Time.now) diff --git a/activemodel/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb index 026cb08a06..1cd4dbc2c5 100644 --- a/activemodel/test/cases/type/unsigned_integer_test.rb +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -1,9 +1,8 @@ require "cases/helper" -require "active_model/type" -module ActiveModel +module ActiveRecord module Type - class UnsignedIntegerTest < ActiveModel::TestCase + class UnsignedIntegerTest < ActiveRecord::TestCase test "unsigned int max value is in range" do assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 66d6ecb928..f5ceb27d97 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -26,7 +26,7 @@ class AssociationValidationTest < ActiveRecord::TestCase def test_validates_associated_one Reply.validates :topic, associated: true - Topic.validates_presence_of( :content ) + Topic.validates_presence_of(:content) r = Reply.new("title" => "A reply", "content" => "with content!") r.topic = Topic.create("title" => "uhohuhoh") assert !r.valid? diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 44b4e28777..28605d2f8e 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -6,6 +6,9 @@ require "models/guid" require "models/event" require "models/dashboard" require "models/uuid_item" +require "models/author" +require "models/person" +require "models/essay" class Wizard < ActiveRecord::Base self.abstract_class = true @@ -163,6 +166,19 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert !r2.valid?, "Saving r2 first time" end + def test_validate_uniqueness_with_polymorphic_object_scope + Essay.validates_uniqueness_of(:name, scope: :writer) + + a = Author.create(name: "Sergey") + p = Person.create(first_name: "Sergey") + + e1 = a.essays.create(name: "Essay") + assert e1.valid?, "Saving e1" + + e2 = p.essays.create(name: "Essay") + assert e2.valid?, "Saving e2" + end + def test_validate_uniqueness_with_composed_attribute_scope r1 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world" assert r1.valid?, "Saving r1" @@ -356,7 +372,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase e2 = Event.create(title: "abcdefgh") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") end @@ -369,13 +385,13 @@ class UniquenessValidationTest < ActiveRecord::TestCase def test_validate_uniqueness_with_limit_and_utf8 if current_adapter?(:SQLite3Adapter) - # Event.title has limit 5, but does SQLite doesn't truncate. + # Event.title has limit 5, but SQLite doesn't truncate. e1 = Event.create(title: "一二三四五六七八") assert e1.valid?, "Could not create an event with a unique 8 characters title" e2 = Event.create(title: "一二三四五六七八") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "一二三四五六七八") end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5d9aa99497..a305aa295a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -167,6 +167,20 @@ class ValidationsTest < ActiveRecord::TestCase assert topic.valid? end + def test_numericality_validation_checks_against_raw_value + klass = Class.new(Topic) do + def self.model_name + 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") + 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? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 0e38cee334..1d21a2454f 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -11,20 +11,21 @@ module ViewBehavior end class Ebook < ActiveRecord::Base + self.table_name = "ebooks'" self.primary_key = "id" end def setup super @connection = ActiveRecord::Base.connection - create_view "ebooks", <<-SQL + create_view "ebooks'", <<-SQL SELECT id, name, status FROM books WHERE format = 'ebook' SQL end def teardown super - drop_view "ebooks" + drop_view "ebooks'" end def test_reading @@ -44,8 +45,7 @@ module ViewBehavior def test_table_exists view_name = Ebook.table_name - # TODO: switch this assertion around once we changed #tables to not return views. - ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } + assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist" end def test_views_ara_valid_data_sources @@ -66,15 +66,20 @@ module ViewBehavior def test_does_not_assume_id_column_as_primary_key model = Class.new(ActiveRecord::Base) do - self.table_name = "ebooks" + self.table_name = "ebooks'" end assert_nil model.primary_key end def test_does_not_dump_view_as_table - schema = dump_table_schema "ebooks" - assert_no_match %r{create_table "ebooks"}, schema + schema = dump_table_schema "ebooks'" + assert_no_match %r{create_table "ebooks'"}, schema end + + private + def quote_table_name(name) + @connection.quote_table_name(name) + end end if ActiveRecord::Base.connection.supports_views? @@ -83,11 +88,11 @@ if ActiveRecord::Base.connection.supports_views? private def create_view(name, query) - @connection.execute "CREATE VIEW #{name} AS #{query}" + @connection.execute "CREATE VIEW #{quote_table_name(name)} AS #{query}" end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name + @connection.execute "DROP VIEW #{quote_table_name(name)}" if @connection.view_exists? name end end @@ -125,8 +130,7 @@ if ActiveRecord::Base.connection.supports_views? def test_table_exists view_name = Paperback.table_name - # TODO: switch this assertion around once we changed #tables to not return views. - ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } + assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist" end def test_column_definitions @@ -150,7 +154,9 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) || + current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300 + class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :books @@ -196,8 +202,8 @@ if ActiveRecord::Base.connection.supports_views? end end end - end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` -end # end fo `if ActiveRecord::Base.connection.supports_views?` + end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` +end # end of `if ActiveRecord::Base.connection.supports_views?` if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && ActiveRecord::Base.connection.supports_materialized_views? @@ -206,11 +212,11 @@ if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && private def create_view(name, query) - @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" + @connection.execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{query}" end def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name + @connection.execute "DROP MATERIALIZED VIEW #{quote_table_name(name)}" if @connection.view_exists? name end end end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 5192e5050a..bfc13d683d 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/author" class YamlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :authors, :posts + fixtures :topics, :authors, :author_addresses, :posts def test_to_yaml_with_time_with_zone_should_not_raise_exception with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do @@ -95,7 +95,7 @@ class YamlSerializationTest < ActiveRecord::TestCase topic = YAML.load(yaml_fixture("rails_4_1")) assert topic.new_record? - assert_equal nil, topic.id + assert_nil topic.id assert_equal "The First Topic", topic.title assert_equal({ omg: :lol }, topic.content) end @@ -123,8 +123,8 @@ class YamlSerializationTest < ActiveRecord::TestCase def yaml_fixture(file_name) path = File.expand_path( - "../../support/yaml_compatibility_fixtures/#{file_name}.yml", - __FILE__ + "../support/yaml_compatibility_fixtures/#{file_name}.yml", + __dir__ ) File.read(path) end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 58e2d45748..4bcb2aeea6 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -77,6 +77,9 @@ connections: postgresql: arunit: min_messages: warning + arunit_without_prepared_statements: + min_messages: warning + prepared_statements: false arunit2: min_messages: warning diff --git a/activerecord/test/config.rb b/activerecord/test/config.rb index 6e2e8b2145..a65e6ff776 100644 --- a/activerecord/test/config.rb +++ b/activerecord/test/config.rb @@ -1,4 +1,4 @@ -TEST_ROOT = File.expand_path(File.dirname(__FILE__)) +TEST_ROOT = __dir__ ASSETS_ROOT = TEST_ROOT + "/assets" FIXTURES_ROOT = TEST_ROOT + "/fixtures" MIGRATIONS_ROOT = TEST_ROOT + "/migrations" diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index b3625ee72e..699623a6f9 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -25,6 +25,7 @@ ddd: name: "Domain-Driven Design" format: "hardcover" status: 2 + read_status: "forgotten" tlg: author_id: 1 diff --git a/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml b/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml new file mode 100644 index 0000000000..6f9da79b45 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml @@ -0,0 +1,3 @@ +one: + id: 1 +two: ['not a hash'] diff --git a/activerecord/test/fixtures/other_dogs.yml b/activerecord/test/fixtures/other_dogs.yml new file mode 100644 index 0000000000..b576861929 --- /dev/null +++ b/activerecord/test/fixtures/other_dogs.yml @@ -0,0 +1,2 @@ +lassie: + id: 1 diff --git a/activerecord/test/fixtures/subscribers.yml b/activerecord/test/fixtures/subscribers.yml index c6a8c2fa24..0f6e0cd48e 100644 --- a/activerecord/test/fixtures/subscribers.yml +++ b/activerecord/test/fixtures/subscribers.yml @@ -6,6 +6,6 @@ second: nick: webster132 name: David Heinemeier Hansson -thrid: +third: nick: swistak name: Marcin Raczkowski
\ No newline at end of file diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 2e703f6219..a76e4b6795 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -22,11 +22,11 @@ class Admin::User < ActiveRecord::Base store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new def phone_number - read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3') + read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/, '(\1) \2-\3') end def phone_number=(value) - write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,"")) + write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/, "")) end def color diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index fab613afd1..2d9cba77e0 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -106,6 +106,7 @@ class Author < ActiveRecord::Base has_many :tags_with_primary_key, through: :posts has_many :books + has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book" has_many :subscriptions, through: :books has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, through: :subscriptions, source: :subscriber diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 17bf3fbcb4..6466e1b341 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -1,5 +1,5 @@ class Book < ActiveRecord::Base - has_many :authors + belongs_to :author has_many :citations, foreign_key: "book1_id" has_many :references, -> { distinct }, through: :citations, source: :reference_of @@ -8,7 +8,7 @@ class Book < ActiveRecord::Base has_many :subscribers, through: :subscriptions enum status: [:proposed, :written, :published] - enum read_status: { unread: 0, reading: 2, read: 3 } + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } enum nullable_status: [:single, :married] enum language: [:english, :spanish, :french], _prefix: :in enum author_visibility: [:visible, :invisible], _prefix: true diff --git a/activerecord/test/models/boolean.rb b/activerecord/test/models/boolean.rb index 7bae22e5f9..0da228aac2 100644 --- a/activerecord/test/models/boolean.rb +++ b/activerecord/test/models/boolean.rb @@ -1,2 +1,5 @@ class Boolean < ActiveRecord::Base + def has_fun + super + end end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 3196207ac9..113d21cb84 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -50,9 +50,3 @@ class FailedBulb < Bulb throw(:abort) end end - -class TrickyBulb < Bulb - after_create do |record| - record.car.bulbs.to_a - end -end diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index e8654dca01..4b2840c653 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -29,6 +29,15 @@ class Category < ActiveRecord::Base has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author scope :general, -> { where(name: "General") } + + # Should be delegated `ast` and `locked` to `arel`. + def self.ast + raise + end + + def self.locked + raise + end end class SpecialCategory < Category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index a4b81d56e0..d227f6fe86 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -19,6 +19,23 @@ class Comment < ActiveRecord::Base has_many :children, class_name: "Comment", foreign_key: :parent_id belongs_to :parent, class_name: "Comment", counter_cache: :children_count + class ::OopsError < RuntimeError; end + + module OopsExtension + def destroy_all(*) + raise OopsError + end + end + + default_scope { extending OopsExtension } + + scope :oops_comments, -> { extending OopsExtension } + + # Should not be called if extending modules that having the method exists on an association. + def self.greeting + raise + end + def self.what_are_you "a comment..." end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 025630087c..d269a95e8c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -151,7 +151,7 @@ class Client < Company # is calling client.destroy, deleting from the database, or setting # foreign keys to NULL. def self.destroyed_client_ids - @destroyed_client_ids ||= Hash.new { |h,k| h[k] = [] } + @destroyed_client_ids ||= Hash.new { |h, k| h[k] = [] } end before_destroy do |client| @@ -170,14 +170,6 @@ class Client < Company def overwrite_to_raise end - - class << self - private - - def private_method - "darkness" - end - end end class ExclusivelyDependentFirm < Company @@ -199,7 +191,7 @@ class Account < ActiveRecord::Base alias_attribute :available_credit, :credit_limit def self.destroyed_account_ids - @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } + @destroyed_account_ids ||= Hash.new { |h, k| h[k] = [] } end # Test private kernel method through collection proxy using has_many. @@ -216,14 +208,12 @@ class Account < ActiveRecord::Base validate :check_empty_credit_limit - protected + private def check_empty_credit_limit errors.add("credit_limit", :blank) if credit_limit.blank? end - private - def private_method "Sir, yes sir!" end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 682f99e365..0782c1eff4 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -88,7 +88,7 @@ module MyApplication validate :check_empty_credit_limit - protected + private def check_empty_credit_limit errors.add("credit_card", :blank) if credit_card.blank? diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 60af7c2247..3d40cb1ace 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -56,7 +56,7 @@ class GpsLocation end def ==(other) - self.latitude == other.latitude && self.longitude == other.longitude + latitude == other.latitude && longitude == other.longitude end end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 5ca1d37f6d..654830ba11 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -251,7 +251,7 @@ class ThreadsafeDeveloper < ActiveRecord::Base self.table_name = "developers" def self.default_scope - sleep 0.05 if Thread.current[:long_default_scope] + Thread.current[:default_scope_delay].call limit(1) end end @@ -260,3 +260,9 @@ class CachedDeveloper < ActiveRecord::Base self.table_name = "developers" self.cache_timestamp_format = :number end + +class DeveloperWithIncorrectlyOrderedHasManyThrough < ActiveRecord::Base + self.table_name = "developers" + has_many :companies, through: :contracts + has_many :contracts, foreign_key: :developer_id +end diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb index 13267fbc21..1f9772870e 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,4 +1,5 @@ class Essay < ActiveRecord::Base + belongs_to :author belongs_to :writer, primary_key: :name, polymorphic: true belongs_to :category, primary_key: :name has_one :owner, primary_key: :name diff --git a/activerecord/test/models/eye.rb b/activerecord/test/models/eye.rb index ab3b3eacf3..f53c34e4b1 100644 --- a/activerecord/test/models/eye.rb +++ b/activerecord/test/models/eye.rb @@ -22,12 +22,12 @@ class Eye < ActiveRecord::Base alias trace_after_create2 trace_after_create def trace_after_update - (@after_update_callbacks_stack ||= []) << iris.changed? + (@after_update_callbacks_stack ||= []) << iris.has_changes_to_save? end alias trace_after_update2 trace_after_update def trace_after_save - (@after_save_callbacks_stack ||= []) << iris.changed? + (@after_save_callbacks_stack ||= []) << iris.has_changes_to_save? end alias trace_after_save2 trace_after_save end diff --git a/activerecord/test/models/family.rb b/activerecord/test/models/family.rb new file mode 100644 index 0000000000..5ae5a78c95 --- /dev/null +++ b/activerecord/test/models/family.rb @@ -0,0 +1,4 @@ +class Family < ActiveRecord::Base + has_many :family_trees, -> { where(token: nil) } + has_many :members, through: :family_trees +end diff --git a/activerecord/test/models/family_tree.rb b/activerecord/test/models/family_tree.rb new file mode 100644 index 0000000000..cd9829fedd --- /dev/null +++ b/activerecord/test/models/family_tree.rb @@ -0,0 +1,4 @@ +class FamilyTree < ActiveRecord::Base + belongs_to :member, class_name: "User", foreign_key: "member_id" + belongs_to :family +end diff --git a/activerecord/test/models/non_primary_key.rb b/activerecord/test/models/non_primary_key.rb new file mode 100644 index 0000000000..1cafb09608 --- /dev/null +++ b/activerecord/test/models/non_primary_key.rb @@ -0,0 +1,2 @@ +class NonPrimaryKey < ActiveRecord::Base +end diff --git a/activerecord/test/models/other_dog.rb b/activerecord/test/models/other_dog.rb new file mode 100644 index 0000000000..418caf34be --- /dev/null +++ b/activerecord/test/models/other_dog.rb @@ -0,0 +1,5 @@ +require_dependency "models/arunit2_model" + +class OtherDog < ARUnit2Model + self.table_name = "dogs" +end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index 5b693664d4..1e5f9285a8 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -13,6 +13,11 @@ class Parrot < ActiveRecord::Base def cancel_save_callback_method throw(:abort) end + + before_update :increment_updated_count + def increment_updated_count + self.updated_count += 1 + end end class LiveParrot < Parrot diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 2dc8f9bd84..c532ab426e 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -9,10 +9,10 @@ class Pirate < ActiveRecord::Base before_remove: :log_before_remove, after_remove: :log_after_remove has_and_belongs_to_many :parrots_with_proc_callbacks, class_name: "Parrot", - before_add: proc { |p,pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}" }, - after_add: proc { |p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}" }, - before_remove: proc { |p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}" }, - after_remove: proc { |p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}" } + before_add: proc { |p, pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}" }, + after_add: proc { |p, pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}" }, + before_remove: proc { |p, pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}" }, + after_remove: proc { |p, pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}" } has_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true has_many :treasures, as: :looter @@ -28,10 +28,10 @@ class Pirate < ActiveRecord::Base before_remove: :log_before_remove, after_remove: :log_after_remove has_many :birds_with_proc_callbacks, class_name: "Bird", - before_add: proc { |p,b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}" }, - after_add: proc { |p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}" }, - before_remove: proc { |p,b| p.ship_log << "before_removing_proc_bird_#{b.id}" }, - after_remove: proc { |p,b| p.ship_log << "after_removing_proc_bird_#{b.id}" } + before_add: proc { |p, b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}" }, + after_add: proc { |p, b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}" }, + before_remove: proc { |p, b| p.ship_log << "before_removing_proc_bird_#{b.id}" }, + after_remove: proc { |p, b| p.ship_log << "after_removing_proc_bird_#{b.id}" } has_many :birds_with_reject_all_blank, class_name: "Bird" has_one :foo_bulb, -> { where name: "foo" }, foreign_key: :car_id, class_name: "Bulb" diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 66a99cbcda..4c913b3b72 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -13,7 +13,7 @@ class Post < ActiveRecord::Base module NamedExtension2 def greeting - "hello" + "hullo" end end @@ -47,7 +47,7 @@ class Post < ActiveRecord::Base scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) } - has_many :comments do + has_many :comments do def find_most_recent order("id DESC").first end @@ -59,6 +59,10 @@ class Post < ActiveRecord::Base def the_association proxy_association end + + def with_content(content) + self.detect { |comment| comment.body == content } + end end has_many :comments_with_extend, extend: NamedExtension, class_name: "Comment", foreign_key: "post_id" do @@ -168,7 +172,7 @@ class Post < ActiveRecord::Base @log = [] end - def self.log(message=nil, side=nil, new_record=nil) + def self.log(message = nil, side = nil, new_record = nil) return @log if message.nil? @log << [message, side, new_record] end @@ -231,7 +235,7 @@ end class SpecialPostWithDefaultScope < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" - default_scope { where(id: [1, 5,6]) } + default_scope { where(id: [1, 5, 6]) } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 5009f8f54b..4fbd986e40 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -11,7 +11,7 @@ class Project < ActiveRecord::Base after_add: Proc.new { |o, r| o.developers_log << "after_adding#{r.id || '<new>'}" }, before_remove: Proc.new { |o, r| o.developers_log << "before_removing#{r.id}" }, after_remove: Proc.new { |o, r| o.developers_log << "after_removing#{r.id}" } - has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer" + has_and_belongs_to_many :well_paid_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer" belongs_to :firm has_one :lead_developer, through: :firm, inverse_of: :contracted_projects diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb index 29e290825e..504f68a296 100644 --- a/activerecord/test/models/subject.rb +++ b/activerecord/test/models/subject.rb @@ -5,7 +5,7 @@ class Subject < ActiveRecord::Base # as otherwise synonym test was failing after_initialize :set_email_address - protected + private def set_email_address unless persisted? self.author_email_address = "test@test.com" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index db04735d01..d9381ac9cf 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -14,7 +14,7 @@ class Topic < ActiveRecord::Base scope :replied, -> { where "replies_count > 0" } scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> { all } do + scope :anonymous_extension, -> {} do def one 1 end @@ -73,7 +73,7 @@ class Topic < ActiveRecord::Base write_attribute(:approved, val) end - protected + private def default_written_on self.written_on = Time.now unless attribute_present?("written_on") diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb index 47649e0a77..5089a795f4 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -5,8 +5,12 @@ class User < ActiveRecord::Base has_secure_token :auth_token has_and_belongs_to_many :jobs_pool, - class_name: Job, + class_name: "Job", join_table: "jobs_pool" + + has_one :family_tree, -> { where(token: nil) }, foreign_key: "member_id" + has_one :family, through: :family_tree + has_many :family_members, through: :family, source: :members end class UserWithNotification < User diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 9a203a7293..90a314c83c 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -6,6 +6,11 @@ ActiveRecord::Schema.define do end end + create_table :timestamp_defaults, force: true do |t| + t.timestamp :nullable_timestamp + t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f00b858ea6..e56e8fa36a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,12 +1,15 @@ ActiveRecord::Schema.define do enable_extension!("uuid-ossp", ActiveRecord::Base.connection) + enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid? - create_table :uuid_parents, id: :uuid, force: true do |t| + uuid_default = connection.supports_pgcrypto_uuid? ? {} : { default: "uuid_generate_v4()" } + + create_table :uuid_parents, id: :uuid, force: true, **uuid_default do |t| t.string :name end - create_table :uuid_children, id: :uuid, force: true do |t| + create_table :uuid_children, id: :uuid, force: true, **uuid_default do |t| t.string :name t.uuid :uuid_parent_id end @@ -22,16 +25,24 @@ ActiveRecord::Schema.define do t.string :char2, limit: 50, default: "a varchar field" t.text :char3, default: "a text field" t.bigint :bigint_default, default: -> { "0::bigint" } - t.text :multiline_default, default: '--- [] + t.text :multiline_default, default: "--- [] + +" + end -' + create_table :postgresql_times, force: true do |t| + t.interval :time_interval + t.interval :scaled_time_interval, precision: 6 end - %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones - postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| - drop_table table_name, if_exists: true + create_table :postgresql_oids, force: true do |t| + t.oid :obj_id end + drop_table "postgresql_timestamp_with_zones", if_exists: true + drop_table "postgresql_partitioned_table", if_exists: true + drop_table "postgresql_partitioned_table_parent", if_exists: true + execute "DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE" execute "CREATE SEQUENCE companies_nonstd_seq START 101 OWNED BY companies.id" execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')" @@ -44,21 +55,6 @@ ActiveRecord::Schema.define do end execute <<_SQL - CREATE TABLE postgresql_times ( - id SERIAL PRIMARY KEY, - time_interval INTERVAL, - scaled_time_interval INTERVAL(6) - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_oids ( - id SERIAL PRIMARY KEY, - obj_id OID - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_timestamp_with_zones ( id SERIAL PRIMARY KEY, time TIMESTAMP WITH TIME ZONE @@ -108,7 +104,7 @@ _SQL end create_table :uuid_items, force: true, id: false do |t| - t.uuid :uuid, primary_key: true + t.uuid :uuid, primary_key: true, **uuid_default t.string :title end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a4756ec75a..50f1d9bfe7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -54,8 +54,8 @@ ActiveRecord::Schema.define do create_table :authors, force: true do |t| t.string :name, null: false - t.integer :author_address_id - t.integer :author_address_extra_id + t.references :author_address + t.references :author_address_extra t.string :organization_id t.string :owned_essay_id end @@ -88,7 +88,7 @@ ActiveRecord::Schema.define do end create_table :books, force: true do |t| - t.integer :author_id + t.references :author t.string :format t.column :name, :string t.column :status, :integer, default: 0 @@ -126,6 +126,9 @@ ActiveRecord::Schema.define do t.timestamps null: false end + create_table :old_cars, id: :integer, force: true do |t| + end + create_table :carriers, force: true create_table :categories, force: true do |t| @@ -197,8 +200,8 @@ ActiveRecord::Schema.define do t.integer :rating, default: 1 t.integer :account_id t.string :description, default: "" - t.index [:firm_id, :type, :rating], name: "company_index" - t.index [:firm_id, :type], name: "company_partial_index", where: "rating > 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 t.index "lower(name)", name: "company_expression_index" if supports_expression_index? end @@ -303,7 +306,7 @@ ActiveRecord::Schema.define do end create_table :engines, force: true do |t| - t.integer :car_id + t.references :car, index: false end create_table :entrants, force: true do |t| @@ -326,6 +329,15 @@ ActiveRecord::Schema.define do create_table :eyes, force: true do |t| end + create_table :families, force: true do |t| + end + + create_table :family_trees, force: true do |t| + t.references :family + t.references :member + t.string :token + end + create_table :funny_jokes, force: true do |t| t.string :name end @@ -401,6 +413,9 @@ ActiveRecord::Schema.define do t.string :name end + create_table :kitchens, force: true do |t| + end + create_table :legacy_things, force: true do |t| t.integer :tps_report_number t.integer :version, null: false, default: 0 @@ -436,10 +451,12 @@ ActiveRecord::Schema.define do end create_table :lock_without_defaults, force: true do |t| + t.column :title, :string t.column :lock_version, :integer end create_table :lock_without_defaults_cust, force: true do |t| + t.column :title, :string t.column :custom_lock_version, :integer end @@ -569,6 +586,7 @@ ActiveRecord::Schema.define do t.column :color, :string t.column :parrot_sti_class, :string t.column :killer_id, :integer + t.column :updated_count, :integer, default: 0 if subsecond_precision_supported? t.column :created_at, :datetime, precision: 0 t.column :created_on, :datetime, precision: 0 @@ -649,8 +667,8 @@ ActiveRecord::Schema.define do end create_table :posts, force: true do |t| - t.integer :author_id - t.string :title, null: false + t.references :author + t.string :title, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) @@ -768,6 +786,10 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :sinks, force: true do |t| + t.references :kitchen + end + create_table :shop_accounts, force: true do |t| t.references :customer t.references :customer_carrier @@ -903,7 +925,6 @@ ActiveRecord::Schema.define do create_table(t, force: true) {} end - # NOTE - the following 4 tables are used by models that have :inverse_of options on the associations create_table :men, force: true do |t| t.string :name end @@ -927,14 +948,14 @@ ActiveRecord::Schema.define do t.integer :zine_id end - create_table :wheels, force: true do |t| - t.references :wheelable, polymorphic: true - end - create_table :zines, force: true do |t| t.string :title end + create_table :wheels, force: true do |t| + t.references :wheelable, polymorphic: true + end + create_table :countries, force: true, id: false, primary_key: "country_id" do |t| t.string :country_id t.string :name @@ -1000,16 +1021,14 @@ ActiveRecord::Schema.define do create_table :records, force: true do |t| end - if supports_foreign_keys? - # fk_test_has_fk should be before fk_test_has_pk - create_table :fk_test_has_fk, force: true do |t| - t.integer :fk_id, null: false + disable_referential_integrity do + create_table :fk_test_has_pk, primary_key: "pk_id", force: :cascade do |t| end - create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| + create_table :fk_test_has_fk, force: true do |t| + t.references :fk, null: false + t.foreign_key :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end - - add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end create_table :overloaded_types, force: true do |t| @@ -1027,6 +1046,10 @@ ActiveRecord::Schema.define do create_table :test_with_keyword_column_name, force: true do |t| t.string :desc end + + create_table :non_primary_keys, force: true, id: false do |t| + t.integer :id + end end Course.connection.create_table :courses, force: true do |t| @@ -1046,3 +1069,5 @@ Professor.connection.create_table :courses_professors, id: false, force: true do t.references :course t.references :professor end + +OtherDog.connection.create_table :dogs, force: true diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb deleted file mode 100644 index cc7c36fe2b..0000000000 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ /dev/null @@ -1,18 +0,0 @@ -ActiveRecord::Schema.define do - execute "DROP TABLE fk_test_has_fk" rescue nil - execute "DROP TABLE fk_test_has_pk" rescue nil - execute <<_SQL - CREATE TABLE 'fk_test_has_pk' ( - 'pk_id' INTEGER NOT NULL PRIMARY KEY - ); -_SQL - - execute <<_SQL - CREATE TABLE 'fk_test_has_fk' ( - 'id' INTEGER NOT NULL PRIMARY KEY, - 'fk_id' INTEGER NOT NULL, - - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') - ); -_SQL -end diff --git a/activerecord/test/support/config.rb b/activerecord/test/support/config.rb index d0717f7b34..aaff408b41 100644 --- a/activerecord/test/support/config.rb +++ b/activerecord/test/support/config.rb @@ -1,5 +1,5 @@ require "yaml" -require "erubis" +require "erb" require "fileutils" require "pathname" @@ -20,13 +20,14 @@ module ARTest FileUtils.cp TEST_ROOT + "/config.example.yml", config_file end - erb = Erubis::Eruby.new(config_file.read) + erb = ERB.new(config_file.read) expand_config(YAML.parse(erb.result(binding)).transform) end def expand_config(config) config["connections"].each do |adapter, connection| - dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"]] + dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"], + ["arunit_without_prepared_statements", "activerecord_unittest"]] dbs.each do |name, dbname| unless connection[name].is_a?(Hash) connection[name] = { "database" => connection[name] } diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index c9260398e2..1a609e13c3 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -2,6 +2,7 @@ require "active_support/logger" require "models/college" require "models/course" require "models/professor" +require "models/other_dog" module ARTest def self.connection_name @@ -9,7 +10,10 @@ module ARTest end def self.connection_config - config["connections"][connection_name] + config.fetch("connections").fetch(connection_name) do + puts "Connection #{connection_name.inspect} not found. Available connections: #{config['connections'].keys.join(', ')}" + exit 1 + end end def self.connect diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index f840783059..4c3eded38a 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,227 +1,74 @@ -* Fix `ActiveSupport::TimeWithZone#in` across DST boundaries. +* Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week. - Previously calls to `in` were being sent to the non-DST aware - method `Time#since` via `method_missing`. It is now aliased to - the DST aware `ActiveSupport::TimeWithZone#+` which handles - transitions across DST boundaries, e.g: + *Shota Iguchi* - Time.zone = "US/Eastern" +* Add default option to class_attribute. Before: - t = Time.zone.local(2016,11,6,1) - # => Sun, 06 Nov 2016 01:00:00 EDT -05:00 + class_attribute :settings + self.settings = {} - t.in(1.hour) - # => Sun, 06 Nov 2016 01:00:00 EST -05:00 + Now: - Fixes #26580. + class_attribute :settings, default: {} - *Thomas Balthazar* + *DHH* -* Remove unused parameter `options = nil` for `#clear` of - `ActiveSupport::Cache::Strategy::LocalCache::LocalStore` and - `ActiveSupport::Cache::Strategy::LocalCache`. +* `#singularize` and `#pluralize` now respect uncountables for the specified locale. - *Yosuke Kabuto* + *Eilis Hamilton* -* Fix `thread_mattr_accessor` subclass no longer overwrites parent. +* 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. - Assigning a value to a subclass using `thread_mattr_accessor` no - longer changes the value of the parent class. This brings the - behavior inline with the documentation. + *DHH* - Given: +* Fix implicit coercion calculations with scalars and durations - class Account - thread_mattr_accessor :user - end + 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: - class Customer < Account - end + 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 - Account.user = "DHH" - Customer.user = "Rafael" + Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain + the part structure of the duration where possible, e.g: - Before: + 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 - Account.user # => "Rafael" - - After: - - Account.user # => "DHH" - - *Shinichi Maeshima* - -* Since weeks are no longer converted to days, add `:weeks` to the list of - parts that `ActiveSupport::TimeWithZone` will recognize as possibly being - of variable duration to take account of DST transitions. - - Fixes #26039. + Fixes #29160, #28970. *Andrew White* -* Defines `Regexp.match?` for Ruby versions prior to 2.4. The predicate - has the same interface, but it does not have the performance boost. Its - purpose is to be able to write 2.4 compatible code. - - *Xavier Noria* - -* Allow `MessageEncryptor` to take advantage of authenticated encryption modes. - - AEAD modes like `aes-256-gcm` provide both confidentiality and data - authenticity, eliminating the need to use `MessageVerifier` to check if the - encrypted data has been tampered with. This speeds up encryption/decryption - and results in shorter cipher text. - - *Bart de Water* - -* Introduce `assert_changes` and `assert_no_changes`. - - `assert_changes` is a more general `assert_difference` that works with any - value. - - assert_changes 'Error.current', from: nil, to: 'ERR' do - expected_bad_operation - end - - Can be called with strings, to be evaluated in the binding (context) of - the block given to the assertion, or a lambda. - - assert_changes -> { Error.current }, from: nil, to: 'ERR' do - expected_bad_operation - end - - The `from` and `to` arguments are compared with the case operator (`===`). - - assert_changes 'Error.current', from: nil, to: Error do - expected_bad_operation - end - - This is pretty useful, if you need to loosely compare a value. For example, - you need to test a token has been generated and it has that many random - characters. - - user = User.start_registration - assert_changes 'user.token', to: /\w{32}/ do - user.finish_registration - end - - *Genadi Samokovarov* - -* Add `:fallback_string` option to `Array#to_sentence`. If an empty array - calls the function and a fallback string option is set then it returns the - fallback string other than an empty string. - - *Mohamed Osama* - -* Fix `ActiveSupport::TimeZone#strptime`. Now raises `ArgumentError` when the - given time doesn't match the format. The error is the same as the one given - by Ruby's `Date.strptime`. Previously it raised - `NoMethodError: undefined method empty? for nil:NilClass.` due to a bug. +* 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. - Fixes #25701. + *DHH* - *John Gesimondo* +* Pass gem name and deprecation horizon to deprecation notifications. -* `travel/travel_to` travel time helpers, now raise on nested calls, - as this can lead to confusing time stubbing. + *Willem van Bergen* - Instead of: +* Add support for `:offset` and `:zone` to `ActiveSupport::TimeWithZone#change` - travel_to 2.days.from_now do - # 2 days from today - travel_to 3.days.from_now do - # 5 days from today - end - end - - preferred way to achieve above is: - - travel 2.days do - # 2 days from today - end - - travel 5.days do - # 5 days from today - end - - *Vipul A M* - -* Support parsing JSON time in ISO8601 local time strings in - `ActiveSupport::JSON.decode` when `parse_json_times` is enabled. - Strings in the format of `YYYY-MM-DD hh:mm:ss` (without a `Z` at - the end) will be parsed in the local timezone (`Time.zone`). In - addition, date strings (`YYYY-MM-DD`) are now parsed into `Date` - objects. - - *Grzegorz Witek* - -* Fixed `ActiveSupport::Logger.broadcast` so that calls to `#silence` now - properly delegate to all loggers. Silencing now properly suppresses logging - to both the log and the console. - - *Kevin McPhillips* - -* Remove deprecated arguments in `assert_nothing_raised`. - - *Rafel Mendonça França* - -* `Date.to_s` doesn't produce too many spaces. For example, `to_s(:short)` - will now produce `01 Feb` instead of ` 1 Feb`. - - Fixes #25251. - - *Sean Griffin* - -* Introduce `Module#delegate_missing_to`. - - When building a decorator, a common pattern emerges: - - class Partition - def initialize(first_event) - @events = [ first_event ] - end - - def people - if @events.first.detail.people.any? - @events.collect { |e| Array(e.detail.people) }.flatten.uniq - else - @events.collect(&:creator).uniq - end - end - - private - def respond_to_missing?(name, include_private = false) - @events.respond_to?(name, include_private) - end - - def method_missing(method, *args, &block) - @events.send(method, *args, &block) - end - end + *Andrew White* - With `Module#delegate_missing_to`, the above is condensed to: +* Add support for `:offset` to `Time#change` - class Partition - delegate_missing_to :@events + Fixes #28723. - def initialize(first_event) - @events = [ first_event ] - end + *Andrew White* - def people - if @events.first.detail.people.any? - @events.collect { |e| Array(e.detail.people) }.flatten.uniq - else - @events.collect(&:creator).uniq - end - end - end +* Add `fetch_values` for `HashWithIndifferentAccess` - *Genadi Samokovarov*, *DHH* + The method was originally added to `Hash` in Ruby 2.3.0. -* Rescuable: If a handler doesn't match the exception, check for handlers - matching the exception's cause. + *Josh Pencheon* - *Jeremy Daer* -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 40235833ba..6b3cead1a7 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2016 David Heinemeier Hansson +Copyright (c) 2005-2017 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/activesupport.gemspec b/activesupport/activesupport.gemspec index 08370cba85..ed277c81ef 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index ef220e0329..6f62593f14 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -1,13 +1,14 @@ #!/usr/bin/env ruby begin - $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) + $:.unshift(File.expand_path("../lib", __dir__)) require "active_support" rescue IOError end require "open-uri" require "tmpdir" +require "fileutils" module ActiveSupport module Multibyte @@ -51,9 +52,9 @@ module ActiveSupport codepoint.code = $1.hex codepoint.combining_class = Integer($4) codepoint.decomp_type = $7 - codepoint.decomp_mapping = ($8=="") ? nil : $8.split.collect(&:hex) - codepoint.uppercase_mapping = ($16=="") ? 0 : $16.hex - codepoint.lowercase_mapping = ($17=="") ? 0 : $17.hex + codepoint.decomp_mapping = ($8 == "") ? nil : $8.split.collect(&:hex) + codepoint.uppercase_mapping = ($16 == "") ? 0 : $16.hex + codepoint.lowercase_mapping = ($17 == "") ? 0 : $17.hex @ucd.codepoints[codepoint.code] = codepoint end @@ -92,7 +93,7 @@ module ActiveSupport end def normalize_boundary_map - @ucd.boundary.each do |k,v| + @ucd.boundary.each do |k, v| if [:lf, :cr].include? k @ucd.boundary[k] = v[0] end @@ -101,9 +102,10 @@ module ActiveSupport def parse SOURCES.each do |type, url| - filename = File.join(Dir.tmpdir, "#{url.split('/').last}") + filename = File.join(Dir.tmpdir, UNICODE_VERSION, "#{url.split('/').last}") unless File.exist?(filename) $stderr.puts "Downloading #{url.split('/').last}" + FileUtils.mkdir_p(File.dirname(filename)) File.open(filename, "wb") do |target| open(url) do |source| source.each_line { |line| target.write line } diff --git a/activesupport/bin/test b/activesupport/bin/test index 84a05bba08..a7beb14b27 100755 --- a/activesupport/bin/test +++ b/activesupport/bin/test @@ -2,5 +2,3 @@ COMPONENT_ROOT = File.expand_path("..", __dir__) require File.expand_path("../tools/test", COMPONENT_ROOT) - -exit Minitest.run(ARGV) diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 2fc42e1975..a667fbb54a 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2016 David Heinemeier Hansson +# Copyright (c) 2005-2017 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -32,6 +32,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker autoload :ExecutionWrapper @@ -80,11 +81,15 @@ module ActiveSupport cattr_accessor :test_order # :nodoc: def self.halt_callback_chains_on_return_false - Callbacks.halt_and_display_warning_on_return_false + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport.halt_callback_chains_on_return_false is deprecated and will be removed in Rails 5.2. + MSG end def self.halt_callback_chains_on_return_false=(value) - Callbacks.halt_and_display_warning_on_return_false = value + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport.halt_callback_chains_on_return_false= is deprecated and will be removed in Rails 5.2. + MSG end def self.to_time_preserves_timezone diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb index 85122e39b2..befa1746c6 100644 --- a/activesupport/lib/active_support/array_inquirer.rb +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -20,7 +20,7 @@ module ActiveSupport # variants.any?(:phone, :tablet) # => true # variants.any?('phone', 'desktop') # => true # variants.any?(:desktop, :watch) # => false - def any?(*candidates, &block) + def any?(*candidates) if candidates.none? super else @@ -32,7 +32,7 @@ module ActiveSupport private def respond_to_missing?(name, include_private = false) - name[-1] == "?" + (name[-1] == "?") || super end def method_missing(name, *args) diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 169a58ecd1..e47c90597f 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -12,7 +12,7 @@ module ActiveSupport # is to exclude the output of a noisy library from the backtrace, so that you # can focus on the rest. # - # bc = BacktraceCleaner.new + # bc = ActiveSupport::BacktraceCleaner.new # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix # bc.add_silencer { |line| line =~ /puma|rubygems/ } # skip any lines from puma or rubygems # bc.clean(exception.backtrace) # perform the cleanup diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index e65e472d08..a1093a2e23 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -1,14 +1,11 @@ -require "benchmark" require "zlib" require "active_support/core_ext/array/extract_options" require "active_support/core_ext/array/wrap" -require "active_support/core_ext/benchmark" require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/numeric/bytes" require "active_support/core_ext/numeric/time" require "active_support/core_ext/object/to_param" require "active_support/core_ext/string/inflections" -require "active_support/core_ext/string/strip" module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -73,8 +70,8 @@ module ActiveSupport # each of elements in the array will be turned into parameters/keys and # concatenated into a single key. For example: # - # expand_cache_key([:foo, :bar]) # => "foo/bar" - # expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" + # ActiveSupport::Cache.expand_cache_key([:foo, :bar]) # => "foo/bar" + # ActiveSupport::Cache.expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" # # The +key+ argument can also respond to +cache_key+ or +to_param+. def expand_cache_key(key, namespace = nil) @@ -91,10 +88,11 @@ module ActiveSupport private def retrieve_cache_key(key) case - 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 + 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 @@ -222,6 +220,10 @@ module ActiveSupport # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes) # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry # + # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt> + # is of the same version. nil is returned on mismatches despite contents. + # This feature is used to support recyclable cache keys. + # # Setting <tt>:race_condition_ttl</tt> is very useful in situations where # a cache entry is used very frequently and is under heavy load. If a # cache expires and due to heavy load several different processes will try @@ -290,6 +292,7 @@ module ActiveSupport instrument(:read, name, options) do |payload| cached_entry = read_entry(key, options) unless options[:force] entry = handle_expired_entry(cached_entry, key, options) + entry = nil if entry && entry.mismatched?(normalize_version(name, options)) payload[:super_operation] = :fetch if payload payload[:hit] = !!entry if payload end @@ -306,21 +309,30 @@ module ActiveSupport end end - # Fetches data from the cache, using the given key. If there is data in + # Reads data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. Otherwise, # +nil+ is returned. # + # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options, + # both of these conditions are applied before the data is returned. + # # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = normalize_key(name, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + instrument(:read, name, options) do |payload| entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) payload[:hit] = false if payload nil + elsif entry.mismatched?(version) + payload[:hit] = false if payload + nil else payload[:hit] = true if payload entry.value @@ -344,11 +356,15 @@ module ActiveSupport results = {} names.each do |name| - key = normalize_key(name, options) - entry = read_entry(key, options) + 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 @@ -399,7 +415,7 @@ module ActiveSupport options = merged_options(options) instrument(:write, name, options) do - entry = Entry.new(value, options) + entry = Entry.new(value, options.merge(version: normalize_version(name, options))) write_entry(normalize_key(name, options), entry, options) end end @@ -423,7 +439,7 @@ module ActiveSupport instrument(:exist?, name) do entry = read_entry(normalize_key(name, options), options) - (entry && !entry.expired?) || false + (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false end end @@ -473,12 +489,12 @@ module ActiveSupport raise NotImplementedError.new("#{self.class.name} does not support clear") end - protected + private # Adds the namespace defined in the options to a pattern designed to # match keys. Implementations that support delete_matched should call # this method to translate a pattern that matches names into one that # matches namespaced keys. - def key_matcher(pattern, options) + def key_matcher(pattern, options) # :doc: prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace] if prefix source = pattern.source @@ -495,25 +511,24 @@ module ActiveSupport # Reads an entry from the cache implementation. Subclasses must implement # this method. - def read_entry(key, options) # :nodoc: + def read_entry(key, options) raise NotImplementedError.new end # Writes an entry to the cache implementation. Subclasses must implement # this method. - def write_entry(key, entry, options) # :nodoc: + def write_entry(key, entry, options) raise NotImplementedError.new end # Deletes an entry from the cache implementation. Subclasses must # implement this method. - def delete_entry(key, options) # :nodoc: + def delete_entry(key, options) raise NotImplementedError.new end - private # Merges the default options with ones specific to a method call. - def merged_options(call_options) # :nodoc: + def merged_options(call_options) if call_options options.merge(call_options) else @@ -521,10 +536,20 @@ 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 = expanded_key(key) + namespace = options[:namespace] if options + prefix = namespace.is_a?(Proc) ? namespace.call : namespace + key = "#{prefix}:#{key}" if prefix + key + 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) # :nodoc: + def expanded_key(key) return key.cache_key.to_s if key.respond_to?(:cache_key) case key @@ -535,28 +560,22 @@ module ActiveSupport key = key.first end when Hash - key = key.sort_by { |k,_| k.to_s }.collect { |k,v| "#{k}=#{v}" } + key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" } end key.to_param end - # Prefixes a key with the namespace. Namespace and key will be delimited - # with a colon. - def normalize_key(key, options) - key = expanded_key(key) - namespace = options[:namespace] if options - prefix = namespace.is_a?(Proc) ? namespace.call : namespace - key = "#{prefix}:#{key}" if prefix - key + def normalize_version(key, options = nil) + (options && options[:version].try(:to_param)) || expanded_version(key) end - def namespaced_key(*args) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - `namespaced_key` is deprecated and will be removed from Rails 5.1. - Please use `normalize_key` which will return a fully resolved key. - MESSAGE - normalize_key(*args) + def expanded_version(key) + case + when key.respond_to?(:cache_version) then key.cache_version.to_param + when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param + when key.respond_to?(:to_a) then expanded_version(key.to_a) + end end def instrument(operation, key, options = nil) @@ -603,13 +622,16 @@ module ActiveSupport end end - # This class is used to represent cache entries. Cache entries have a value and an optional - # expiration time. The expiration time is used to support the :race_condition_ttl option - # on the cache. + # This class is used to represent cache entries. Cache entries have a value, an optional + # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option + # on the cache. The version is used to support the :version option on the cache for rejecting + # mismatches. # # Since cache entries in most instances will be serialized, the internals of this class are highly optimized # using short instance variable names that are lazily defined. class Entry # :nodoc: + attr_reader :version + DEFAULT_COMPRESS_LIMIT = 16.kilobytes # Creates a new cache entry for the specified value. Options supported are @@ -622,6 +644,7 @@ module ActiveSupport @value = value end + @version = options[:version] @created_at = Time.now.to_f @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @@ -631,6 +654,10 @@ module ActiveSupport compressed? ? uncompress(@value) : @value end + def mismatched?(version) + @version && version && @version != version + end + # Checks if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 1971ff182e..d5c8585816 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -66,7 +66,7 @@ module ActiveSupport end end - protected + private def read_entry(key, options) if File.exist?(key) @@ -98,9 +98,8 @@ module ActiveSupport end end - private # Lock a file for a block so only one process can modify it at a time. - def lock_file(file_name, &block) # :nodoc: + def lock_file(file_name, &block) if File.exist?(file_name) File.open(file_name, "r+") do |f| begin @@ -138,14 +137,6 @@ module ActiveSupport File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) end - def key_file_path(key) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - `key_file_path` is deprecated and will be removed from Rails 5.1. - Please use `normalize_key` which will return a fully resolved key or nothing. - MESSAGE - key - end - # Translate a file path into a key. def file_path_key(path) fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index cfd5e39bc4..06fa9f67ad 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -26,7 +26,7 @@ module ActiveSupport class MemCacheStore < Store # Provide support for raw values in the local cache strategy. module LocalCacheWithRaw # :nodoc: - protected + private def read_entry(key, options) entry = super if options[:raw] && local_cache && entry @@ -35,7 +35,7 @@ module ActiveSupport entry end - def write_entry(key, entry, options) # :nodoc: + def write_entry(key, entry, options) if options[:raw] && local_cache raw_entry = Entry.new(entry.value.to_s) raw_entry.expires_at = entry.expires_at @@ -97,12 +97,18 @@ module ActiveSupport 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, raw: true) + + raw_values = @data.get_multi(keys_to_names.keys) values = {} + raw_values.each do |key, value| entry = deserialize_entry(value) - values[keys_to_names[key]] = entry.value unless entry.expired? + + unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) + values[keys_to_names[key]] = entry.value + end end + values end @@ -110,7 +116,7 @@ module ActiveSupport # 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. - def increment(name, amount = 1, options = nil) # :nodoc: + def increment(name, amount = 1, options = nil) options = merged_options(options) instrument(:increment, name, amount: amount) do rescue_error_with nil do @@ -123,7 +129,7 @@ module ActiveSupport # 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. - def decrement(name, amount = 1, options = nil) # :nodoc: + def decrement(name, amount = 1, options = nil) options = merged_options(options) instrument(:decrement, name, amount: amount) do rescue_error_with nil do @@ -143,14 +149,14 @@ module ActiveSupport @data.stats end - protected + private # Read an entry from the cache. - def read_entry(key, options) # :nodoc: + def read_entry(key, options) rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) } end # Write an entry to the cache. - def write_entry(key, entry, options) # :nodoc: + def write_entry(key, entry, options) method = options && options[:unless_exist] ? :add : :set value = options[:raw] ? entry.value.to_s : entry expires_in = options[:expires_in].to_i @@ -164,12 +170,10 @@ module ActiveSupport end # Delete an entry from the cache. - def delete_entry(key, options) # :nodoc: + def delete_entry(key, options) rescue_error_with(false) { @data.delete(key) } end - private - # Memcache keys are binaries. So we need to force their encoding to binary # before applying the regular expression to ensure we are escaping all # characters properly. @@ -181,14 +185,6 @@ module ActiveSupport key end - def escape_key(key) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - `escape_key` is deprecated and will be removed from Rails 5.1. - Please use `normalize_key` which will return a fully resolved key or nothing. - MESSAGE - key - end - def deserialize_entry(raw_value) if raw_value entry = Marshal.load(raw_value) rescue raw_value diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 1a8477f9fe..56fe1457d0 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -28,6 +28,7 @@ module ActiveSupport @pruning = false end + # Delete all data stored in a given cache store. def clear(options = nil) synchronize do @data.clear @@ -57,7 +58,7 @@ module ActiveSupport start_time = Time.now cleanup instrument(:prune, target_size, from: @cache_size) do - keys = synchronize { @key_access.keys.sort { |a,b| @key_access[a].to_f <=> @key_access[b].to_f } } + keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } } keys.each do |key| delete_entry(key, options) return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time) @@ -83,6 +84,7 @@ module ActiveSupport modify_value(name, -amount, options) end + # Deletes cache entries if the cache key matches a given pattern. def delete_matched(matcher, options = nil) options = merged_options(options) instrument(:delete_matched, matcher.inspect) do @@ -104,15 +106,15 @@ module ActiveSupport @monitor.synchronize(&block) end - protected + private PER_ENTRY_OVERHEAD = 240 - def cached_size(key, entry) # :nodoc: + def cached_size(key, entry) key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD end - def read_entry(key, options) # :nodoc: + def read_entry(key, options) entry = @data[key] synchronize do if entry @@ -124,7 +126,7 @@ module ActiveSupport entry end - def write_entry(key, entry, options) # :nodoc: + def write_entry(key, entry, options) entry.dup_value! synchronize do old_entry = @data[key] @@ -141,7 +143,7 @@ module ActiveSupport end end - def delete_entry(key, options) # :nodoc: + def delete_entry(key, options) synchronize do @key_access.delete(key) entry = @data.delete(key) @@ -150,8 +152,6 @@ module ActiveSupport end end - private - def modify_value(name, amount, options) synchronize do options = merged_options(options) diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb index 0564ce5312..550659fc56 100644 --- a/activesupport/lib/active_support/cache/null_store.rb +++ b/activesupport/lib/active_support/cache/null_store.rb @@ -25,15 +25,15 @@ module ActiveSupport def delete_matched(matcher, options = nil) end - protected - def read_entry(key, options) # :nodoc: + private + def read_entry(key, options) end - def write_entry(key, entry, options) # :nodoc: + def write_entry(key, entry, options) true end - def delete_entry(key, options) # :nodoc: + def delete_entry(key, options) false end end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index ec2e96a106..91875a56f5 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -70,6 +70,7 @@ module ActiveSupport def with_local_cache use_temporary_local_cache(LocalStore.new) { yield } end + # Middleware class can be inserted as a Rack handler to be local cache for the # duration of request. def middleware @@ -104,8 +105,8 @@ module ActiveSupport value end - protected - def read_entry(key, options) # :nodoc: + private + def read_entry(key, options) if cache = local_cache cache.fetch_entry(key) { super } else @@ -113,25 +114,22 @@ module ActiveSupport end end - def write_entry(key, entry, options) # :nodoc: - local_cache.write_entry(key, entry, options) if local_cache + def write_entry(key, entry, options) + if options[:unless_exist] + local_cache.delete_entry(key, options) if local_cache + else + local_cache.write_entry(key, entry, options) if local_cache + end + super end - def delete_entry(key, options) # :nodoc: + def delete_entry(key, options) local_cache.delete_entry(key, options) if local_cache super end - def set_cache_value(value, name, amount, options) # :nodoc: - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - `set_cache_value` is deprecated and will be removed from Rails 5.1. - Please use `write_cache_value` instead. - MESSAGE - write_cache_value name, value, options - end - - def write_cache_value(name, value, options) # :nodoc: + def write_cache_value(name, value, options) name = normalize_key(name, options) cache = local_cache cache.mute do @@ -143,8 +141,6 @@ module ActiveSupport end end - private - def local_cache_key @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb index af51f66dda..4c3679e4bf 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb @@ -12,9 +12,9 @@ module ActiveSupport attr_reader :name, :local_cache_key def initialize(name, local_cache_key) - @name = name + @name = name @local_cache_key = local_cache_key - @app = nil + @app = nil end def new(app) @@ -28,13 +28,13 @@ module ActiveSupport response[2] = ::Rack::BodyProxy.new(response[2]) do LocalCacheRegistry.set_cache_for(local_cache_key, nil) end + cleanup_on_body_close = true response rescue Rack::Utils::InvalidParameterError - LocalCacheRegistry.set_cache_for(local_cache_key, nil) [400, {}, []] - rescue Exception - LocalCacheRegistry.set_cache_for(local_cache_key, nil) - raise + ensure + LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless + cleanup_on_body_close end end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 6d39be1c1f..ddfa91a342 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,7 +4,6 @@ require "active_support/core_ext/array/extract_options" require "active_support/core_ext/class/attribute" require "active_support/core_ext/kernel/reporting" require "active_support/core_ext/kernel/singleton_class" -require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/string/filters" require "active_support/deprecation" require "thread" @@ -63,16 +62,11 @@ module ActiveSupport included do extend ActiveSupport::DescendantsTracker + class_attribute :__callbacks, instance_writer: false, default: {} end CALLBACK_FILTER_TYPES = [:before, :after, :around] - # If true, Active Record and Active Model callbacks returning +false+ will - # halt the entire callback chain and display a deprecation message. - # If false, callback chains will only be halted by calling +throw :abort+. - # Defaults to +true+. - mattr_accessor(:halt_and_display_warning_on_return_false, instance_writer: false) { true } - # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -86,21 +80,63 @@ module ActiveSupport # run_callbacks :save do # save # end - def run_callbacks(kind, &block) - send "_run_#{kind}_callbacks", &block - end - - private + # + #-- + # + # As this method is used in many places, and often wraps large portions of + # user code, it has an additional design goal of minimizing its impact on + # the visible call stack. An exception from inside a :before or :after + # callback can be as noisy as it likes -- but when control has passed + # smoothly through and into the supplied block, we want as little evidence + # as possible that we were here. + def run_callbacks(kind) + callbacks = __callbacks[kind.to_sym] + + if callbacks.empty? + yield if block_given? + else + env = Filters::Environment.new(self, false, nil) + next_sequence = callbacks.compile + + invoke_sequence = Proc.new do + skipped = nil + while true + current = next_sequence + current.invoke_before(env) + if current.final? + env.value = !env.halted && (!block_given? || yield) + elsif current.skip?(env) + (skipped ||= []) << current + next_sequence = next_sequence.nested + next + else + next_sequence = next_sequence.nested + begin + target, block, method, *arguments = current.expand_call_template(env, invoke_sequence) + target.send(method, *arguments, &block) + ensure + next_sequence = current + end + end + current.invoke_after(env) + skipped.pop.invoke_after(env) while skipped && skipped.first + break env.value + end + end - def __run_callbacks__(callbacks, &block) - if callbacks.empty? - yield if block_given? + # Common case: no 'around' callbacks defined + if next_sequence.final? + next_sequence.invoke_before(env) + env.value = !env.halted && (!block_given? || yield) + next_sequence.invoke_after(env) + env.value else - runner = callbacks.compile - e = Filters::Environment.new(self, false, nil, block) - runner.call(e).value + invoke_sequence.call end end + end + + private # A hook invoked every time a before callback is halted. # This can be overridden in ActiveSupport::Callbacks implementors in order @@ -118,16 +154,7 @@ module ActiveSupport end module Filters - Environment = Struct.new(:target, :halted, :value, :run_block) - - class End - def call(env) - block = env.run_block - env.value = !env.halted && (!block || block.call) - env - end - end - ENDING = End.new + Environment = Struct.new(:target, :halted, :value) class Before def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter) @@ -246,59 +273,14 @@ module ActiveSupport end private_class_method :simple end - - class Around - def self.build(callback_sequence, user_callback, user_conditions, chain_config) - if user_conditions.any? - halting_and_conditional(callback_sequence, user_callback, user_conditions) - else - halting(callback_sequence, user_callback) - end - end - - def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) - callback_sequence.around do |env, &run| - target = env.target - value = env.value - halted = env.halted - - if !halted && user_conditions.all? { |c| c.call(target, value) } - user_callback.call(target, value) { - run.call.value - } - env - else - run.call - end - end - end - private_class_method :halting_and_conditional - - def self.halting(callback_sequence, user_callback) - callback_sequence.around do |env, &run| - target = env.target - value = env.value - - if env.halted - run.call - else - user_callback.call(target, value) { - run.call.value - } - env - end - end - end - private_class_method :halting - end end class Callback #:nodoc:# def self.build(chain, filter, kind, options) if filter.is_a?(String) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing string to define callback is deprecated and will be removed - in Rails 5.1 without replacement. + raise ArgumentError, <<-MSG.squish + Passing string to define a callback is not supported. See the `.set_callback` + documentation to see supported values. MSG end @@ -309,7 +291,7 @@ module ActiveSupport attr_reader :chain_config def initialize(name, filter, kind, options, chain_config) - @chain_config = chain_config + @chain_config = chain_config @name = name @kind = kind @filter = filter @@ -349,64 +331,23 @@ module ActiveSupport # Wraps code with filter def apply(callback_sequence) user_conditions = conditions_lambdas - user_callback = make_lambda @filter + user_callback = CallTemplate.build(@filter, self) case kind when :before - Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter) + Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter) when :after - Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config) + Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config) when :around - Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config) + callback_sequence.around(user_callback, user_conditions) end end - private - - def invert_lambda(l) - lambda { |*args, &blk| !l.call(*args, &blk) } - end - - # Filters support: - # - # Symbols:: A method to call. - # Strings:: Some content to evaluate. - # Procs:: A proc to call with the object. - # Objects:: An object with a <tt>before_foo</tt> method on it to call. - # - # All of these objects are converted into a lambda and handled - # the same after this point. - def make_lambda(filter) - case filter - when Symbol - lambda { |target, _, &blk| target.send filter, &blk } - when String - l = eval "lambda { |value| #{filter} }" - lambda { |target, value| target.instance_exec(value, &l) } - when Conditionals::Value then filter - when ::Proc - if filter.arity > 1 - return lambda { |target, _, &block| - raise ArgumentError unless block - target.instance_exec(target, block, &filter) - } - end - - if filter.arity <= 0 - lambda { |target, _| target.instance_exec(&filter) } - else - lambda { |target, _| target.instance_exec(target, &filter) } - end - else - scopes = Array(chain_config[:scope]) - method_to_call = scopes.map { |s| public_send(s) }.join("_") - - lambda { |target, _, &blk| - filter.public_send method_to_call, target, &blk - } - end - end + def current_scopes + Array(chain_config[:scope]).map { |s| public_send(s) } + end + private def compute_identifier(filter) case filter when String, ::Proc @@ -417,17 +358,116 @@ module ActiveSupport end def conditions_lambdas - @if.map { |c| make_lambda c } + - @unless.map { |c| invert_lambda make_lambda c } + @if.map { |c| CallTemplate.build(c, self).make_lambda } + + @unless.map { |c| CallTemplate.build(c, self).inverted_lambda } + end + end + + # A future invocation of user-supplied code (either as a callback, + # or a condition filter). + class CallTemplate # :nodoc: + def initialize(target, method, arguments, block) + @override_target = target + @method_name = method + @arguments = arguments + @override_block = block + end + + # Return the parts needed to make this call, with the given + # input values. + # + # Returns an array of the form: + # + # [target, block, method, *arguments] + # + # This array can be used as such: + # + # target.send(method, *arguments, &block) + # + # The actual invocation is left up to the caller to minimize + # call stack pollution. + def expand(target, value, block) + result = @arguments.map { |arg| + case arg + when :value; value + when :target; target + when :block; block || raise(ArgumentError) + end + } + + result.unshift @method_name + result.unshift @override_block || block + result.unshift @override_target || target + + # target, block, method, *arguments = result + # target.send(method, *arguments, &block) + result + end + + # Return a lambda that will make this call when given the input + # values. + def make_lambda + lambda do |target, value, &block| + target, block, method, *arguments = expand(target, value, block) + target.send(method, *arguments, &block) + end + end + + # Return a lambda that will make this call when given the input + # values, but then return the boolean inverse of that result. + def inverted_lambda + lambda do |target, value, &block| + target, block, method, *arguments = expand(target, value, block) + ! target.send(method, *arguments, &block) + end + end + + # Filters support: + # + # Symbols:: A method to call. + # Strings:: Some content to evaluate. + # Procs:: A proc to call with the object. + # Objects:: An object with a <tt>before_foo</tt> method on it to call. + # + # All of these objects are converted into a CallTemplate and handled + # the same after this point. + def self.build(filter, callback) + case filter + when Symbol + new(nil, filter, [], nil) + when String + new(nil, :instance_exec, [:value], compile_lambda(filter)) + when Conditionals::Value + new(filter, :call, [:target, :value], nil) + when ::Proc + if filter.arity > 1 + new(nil, :instance_exec, [:target, :block], filter) + elsif filter.arity > 0 + new(nil, :instance_exec, [:target], filter) + else + new(nil, :instance_exec, [], filter) + end + else + method_to_call = callback.current_scopes.join("_") + + new(filter, method_to_call, [:target], nil) end + end + + def self.compile_lambda(filter) + eval("lambda { |value| #{filter} }") + end end # Execute before and after filters in a sequence instead of # chaining them with nested lambda calls, see: # https://github.com/rails/rails/issues/18011 - class CallbackSequence - def initialize(&call) - @call = call + class CallbackSequence # :nodoc: + def initialize(nested = nil, call_template = nil, user_conditions = nil) + @nested = nested + @call_template = call_template + @user_conditions = user_conditions + @before = [] @after = [] end @@ -442,23 +482,35 @@ module ActiveSupport self end - def around(&around) - CallbackSequence.new do |arg| - around.call(arg) { - call(arg) - } - end + def around(call_template, user_conditions) + CallbackSequence.new(self, call_template, user_conditions) end - def call(arg) + def skip?(arg) + arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) } + end + + def nested + @nested + end + + def final? + !@call_template + end + + def expand_call_template(arg, block) + @call_template.expand(arg.target, arg.value, block) + end + + def invoke_before(arg) @before.each { |b| b.call(arg) } - value = @call.call(arg) + end + + def invoke_after(arg) @after.each { |a| a.call(arg) } - value end end - # An Array with a compile method. class CallbackChain #:nodoc:# include Enumerable @@ -503,7 +555,7 @@ module ActiveSupport def compile @callbacks || @mutex.synchronize do - final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) } + final_sequence = CallbackSequence.new @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| callback.apply callback_sequence end @@ -582,9 +634,8 @@ module ActiveSupport # set_callback :save, :before_method # # The callback can be specified as a symbol naming an instance method; as a - # proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an - # object that responds to a certain method determined by the <tt>:scope</tt> - # argument to +define_callbacks+. + # proc, lambda, or block; or as an object that responds to a certain method + # determined by the <tt>:scope</tt> argument to +define_callbacks+. # # If a proc, lambda, or block is given, its body is evaluated in the context # of the current object. It can also optionally accept the current object as @@ -598,16 +649,24 @@ module ActiveSupport # # ===== Options # - # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings, + # * <tt>:if</tt> - A symbol, a string (deprecated) or an array of symbols, # each naming an instance method or a proc; the callback will be called # only when they all return a true value. - # * <tt>:unless</tt> - A symbol, a string or an array of symbols and - # strings, each naming an instance method or a proc; the callback will - # be called only when they all return a false value. + # * <tt>:unless</tt> - A symbol, a string (deprecated) or an array of symbols, + # each naming an instance method or a proc; the callback will be called + # only when they all return a false value. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + + if options[:if].is_a?(String) || options[:unless].is_a?(String) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing string to :if and :unless conditional options is deprecated + and will be removed in Rails 5.2 without replacement. + MSG + end + self_chain = get_callbacks name mapped = filters.map do |filter| Callback.build(self_chain, filter, type, options) @@ -631,6 +690,14 @@ module ActiveSupport # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>). def skip_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + + if options[:if].is_a?(String) || options[:unless].is_a?(String) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing string to :if and :unless conditional options is deprecated + and will be removed in Rails 5.2 without replacement. + MSG + end + options[:raise] = true unless options.key?(:raise) __update_callbacks(name) do |target, chain| @@ -747,12 +814,25 @@ module ActiveSupport options = names.extract_options! names.each do |name| - class_attribute "_#{name}_callbacks", instance_writer: false + name = name.to_sym + set_callbacks name, CallbackChain.new(name, options) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) - __run_callbacks__(_#{name}_callbacks, &block) + run_callbacks #{name.inspect}, &block + end + + def self._#{name}_callbacks + get_callbacks(#{name.inspect}) + end + + def self._#{name}_callbacks=(value) + set_callbacks(#{name.inspect}, value) + end + + def _#{name}_callbacks + __callbacks[#{name.inspect}] end RUBY end @@ -761,35 +841,11 @@ module ActiveSupport protected def get_callbacks(name) # :nodoc: - send "_#{name}_callbacks" + __callbacks[name.to_sym] end def set_callbacks(name, callbacks) # :nodoc: - send "_#{name}_callbacks=", callbacks - end - - def deprecated_false_terminator # :nodoc: - Proc.new do |target, result_lambda| - terminate = true - catch(:abort) do - result = result_lambda.call if result_lambda.is_a?(Proc) - if Callbacks.halt_and_display_warning_on_return_false && result == false - display_deprecation_warning_for_false_terminator - else - terminate = false - end - end - terminate - end - end - - private - - def display_deprecation_warning_for_false_terminator - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1. - To explicitly halt the callback chain, please use `throw :abort` instead. - MSG + self.__callbacks = __callbacks.merge(name.to_sym => callbacks) end end end diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb deleted file mode 100644 index 53e09b685c..0000000000 --- a/activesupport/lib/active_support/concurrency/latch.rb +++ /dev/null @@ -1,25 +0,0 @@ -require "concurrent/atomic/count_down_latch" - -module ActiveSupport - module Concurrency - class Latch - def initialize(count = 1) - if count == 1 - ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::Event instead.") - else - ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.") - end - - @inner = Concurrent::CountDownLatch.new(count) - end - - def release - @inner.count_down - end - - def await - @inner.wait(nil) - end - end - end -end diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index 52706c3d7a..42e0acf66a 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,4 +1,3 @@ -DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"] -(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path| +Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path| require path end diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 37d833887a..fca33c9d69 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -31,7 +31,7 @@ class Array # # people = ["David", "Rafael", "Aaron", "Todd"] # people.without "Aaron", "Todd" - # => ["David", "Rafael"] + # # => ["David", "Rafael"] # # Note: This is an optimization of `Enumerable#without` that uses `Array#-` # instead of `Array#reject` for performance reasons. diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 1d4c83ae52..cac15e1100 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -40,12 +40,6 @@ class Array # ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ') # # => "one or two or at least three" # - # [].to_sentence(fallback_string: 'none') - # # => "none" - # - # ['one', 'two'].to_sentence(fallback_string: 'none') - # # => "one and two" - # # Using <tt>:locale</tt> option: # # # Given this locale dictionary: @@ -63,7 +57,7 @@ class Array # ['uno', 'dos', 'tres'].to_sentence(locale: :es) # # => "uno o dos o al menos tres" def to_sentence(options = {}) - options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale, :fallback_string) + options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale) default_connectors = { words_connector: ", ", @@ -78,7 +72,7 @@ class Array case length when 0 - "#{options[:fallback_string] || ''}" + "" when 1 "#{self[0]}" when 2 diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index ea9d85f6e3..0d798e5c4e 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -89,7 +89,7 @@ class Array # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] def split(value = nil) - arr = self.dup + arr = dup result = [] if block_given? while (idx = arr.index { |i| yield i }) diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb index 16a6789f8d..88a34128c9 100644 --- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb +++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb @@ -1,7 +1,7 @@ class Array # The human way of thinking about adding stuff to the end of a list is with append. - alias_method :append, :<< + alias_method :append, :push unless [].respond_to?(:append) # The human way of thinking about adding stuff to the beginning of a list is with prepend. - alias_method :prepend, :unshift + alias_method :prepend, :unshift unless [].respond_to?(:prepend) end diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 9b4a9a9992..8caddcd5c3 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -20,7 +20,7 @@ class Class # Base.setting # => true # # In the above case as long as Subclass does not assign a value to setting - # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt> + # by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt> # would read value assigned to parent class. Once Subclass assigns a value then # the value assigned by Subclass would be returned. # @@ -68,11 +68,16 @@ class Class # object.setting = false # => NoMethodError # # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>. + # + # To set a default value for the attribute, pass <tt>default:</tt>, like so: + # + # class_attribute :settings, default: {} def class_attribute(*attrs) options = attrs.extract_options! - instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) - instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) + instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) + instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) instance_predicate = options.fetch(:instance_predicate, true) + default_value = options.fetch(:default, nil) attrs.each do |name| remove_possible_singleton_method(name) @@ -123,6 +128,10 @@ class Class remove_possible_method "#{name}=" attr_writer name end + + unless default_value.nil? + self.send("#{name}=", default_value) + end end end end diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 10a7c787f6..62397d9508 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -22,6 +22,7 @@ class Class def descendants descendants = [] ObjectSpace.each_object(singleton_class) do |k| + next if k.singleton_class? descendants.unshift k unless k == self end descendants diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 2e64097933..d6f60cac04 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -133,7 +133,7 @@ class Date # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there. def compare_with_coercion(other) if other.is_a?(Time) - self.to_datetime <=> other + to_datetime <=> other else compare_without_coercion(other) end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index d553406dff..0f59c754fe 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -79,6 +79,9 @@ class Date # date.to_time(:local) # => 2007-11-10 00:00:00 0800 # # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC + # + # NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ']. + # If the *application's* timezone is needed, then use +in_time_zone+ instead. def to_time(form = :local) raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form) ::Time.send(form, year, month, day) 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 792076a449..e2e1d3e359 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 @@ -300,7 +300,7 @@ module DateAndTime end # Returns a Range representing the whole week of the current date/time. - # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set. + # Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set. def all_week(start_day = Date.beginning_of_week) beginning_of_week(start_day)..end_of_week(start_day) end @@ -320,6 +320,22 @@ module DateAndTime beginning_of_year..end_of_year end + # Returns specific next occurring day of week + 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) + end + + # Returns specific previous occurring day of week + 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) + end + private def first_hour(date_or_time) date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time @@ -334,7 +350,7 @@ module DateAndTime end def copy_time_to(other) - other.change(hour: hour, min: min, sec: sec, usec: try(:usec)) + other.change(hour: hour, min: min, sec: sec, nsec: try(:nsec)) end end end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb index 3f4e236ab7..ab80392460 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb @@ -10,9 +10,5 @@ module DateAndTime # this behavior, but new apps will have an initializer that sets # this to true, because the new behavior is preferred. mattr_accessor(:preserve_timezone, instance_writer: false) { false } - - def to_time - preserve_timezone ? getlocal(utc_offset) : getlocal - end end end diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 70d5c9af8e..7a9eb8c266 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -47,13 +47,23 @@ class DateTime # DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => DateTime.new(1981, 8, 1, 22, 35, 0) # DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0) def change(options) + if new_nsec = options[:nsec] + raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] + new_fraction = Rational(new_nsec, 1000000000) + else + new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + new_fraction = Rational(new_usec, 1000000) + end + + raise ArgumentError, "argument out of range" if new_fraction >= 1 + ::DateTime.civil( options.fetch(:year, year), options.fetch(:month, month), options.fetch(:day, day), options.fetch(:hour, hour), options.fetch(:min, options[:hour] ? 0 : min), - options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction), + options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction, options.fetch(:offset, offset), options.fetch(:start, start) ) @@ -122,7 +132,7 @@ class DateTime # Returns a new DateTime representing the end of the day (23:59:59). def end_of_day - change(hour: 23, min: 59, sec: 59) + change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000)) end alias :at_end_of_day :end_of_day @@ -134,7 +144,7 @@ class DateTime # Returns a new DateTime representing the end of the hour (hh:59:59). def end_of_hour - change(min: 59, sec: 59) + change(min: 59, sec: 59, usec: Rational(999999999, 1000)) end alias :at_end_of_hour :end_of_hour @@ -146,7 +156,7 @@ class DateTime # Returns a new DateTime representing the end of the minute (hh:mm:59). def end_of_minute - change(sec: 59) + change(sec: 59, usec: Rational(999999999, 1000)) end alias :at_end_of_minute :end_of_minute 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 30bb7f4a60..870391aeaa 100644 --- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb @@ -1,5 +1,16 @@ require "active_support/core_ext/date_and_time/compatibility" +require "active_support/core_ext/module/remove_method" class DateTime - prepend DateAndTime::Compatibility + include DateAndTime::Compatibility + + remove_possible_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 + # on the setting of +ActiveSupport.to_time_preserves_timezone+. + def to_time + preserve_timezone ? getlocal(utc_offset) : getlocal + end end diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 44ae96dbe8..d9b3743858 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -64,7 +64,7 @@ class DateTime # # => Sun, 01 Jan 2012 00:00:00 +0300 # DateTime.civil_from_format :local, 2012, 12, 17 # # => Mon, 17 Dec 2012 00:00:00 +0000 - def self.civil_from_format(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0) + def self.civil_from_format(utc_or_local, year, month = 1, day = 1, hour = 0, min = 0, sec = 0) if utc_or_local.to_sym == :local offset = ::Time.local(year, month, day).utc_offset.to_r / 86400 else diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 8ebe758078..3a4ae6cb8b 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,37 +1,59 @@ module Enumerable - # Calculates a sum from the elements. + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements + # when we omit an identity. # - # payments.sum { |p| p.price * p.tax_rate } - # payments.sum(&:price) - # - # The latter is a shortcut for: - # - # payments.inject(0) { |sum, p| sum + p.price } - # - # It can also calculate the sum without the use of a block. - # - # [5, 15, 10].sum # => 30 - # ['foo', 'bar'].sum # => "foobar" - # [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5] - # - # The default sum of an empty list is zero. You can override this default: - # - # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) - def sum(identity = nil, &block) - if block_given? - map(&block).sum(identity) - else - sum = identity ? inject(identity, :+) : inject(:+) - sum || identity || 0 + # We tried shimming it to attempt the fast native method, rescue TypeError, + # and fall back to the compatible implementation, but that's much slower than + # just calling the compat method in the first place. + if Enumerable.instance_methods(false).include?(:sum) && !((?a..?b).sum rescue false) + # We can't use Refinements here because Refinements with Module which will be prepended + # doesn't work well https://bugs.ruby-lang.org/issues/13446 + alias :_original_sum_with_required_identity :sum + private :_original_sum_with_required_identity + # Calculates a sum from the elements. + # + # payments.sum { |p| p.price * p.tax_rate } + # payments.sum(&:price) + # + # The latter is a shortcut for: + # + # payments.inject(0) { |sum, p| sum + p.price } + # + # It can also calculate the sum without the use of a block. + # + # [5, 15, 10].sum # => 30 + # ['foo', 'bar'].sum # => "foobar" + # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5] + # + # The default sum of an empty list is zero. You can override this default: + # + # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) + def sum(identity = nil, &block) + if identity + _original_sum_with_required_identity(identity, &block) + elsif block_given? + map(&block).sum(identity) + else + inject(:+) || 0 + end + end + else + def sum(identity = nil, &block) + if block_given? + map(&block).sum(identity) + else + sum = identity ? inject(identity, :+) : inject(:+) + sum || identity || 0 + end end end # Convert an enumerable to a hash. # # people.index_by(&:login) - # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...} + # # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...} # people.index_by { |person| "#{person.first_name} #{person.last_name}" } - # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} + # # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} def index_by if block_given? result = {} @@ -67,10 +89,10 @@ module Enumerable # Returns a copy of the enumerable without the specified elements. # # ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd" - # => ["David", "Rafael"] + # # => ["David", "Rafael"] # # {foo: 1, bar: 2, baz: 3}.without :bar - # => {foo: 1, baz: 3} + # # => {foo: 1, baz: 3} def without(*elements) reject { |element| elements.include?(element) } end @@ -78,10 +100,10 @@ module Enumerable # Convert an enumerable to an array based on the given key. # # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) - # => ["David", "Rafael", "Aaron"] + # # => ["David", "Rafael", "Aaron"] # # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name) - # => [[1, "David"], [2, "Rafael"]] + # # => [[1, "David"], [2, "Rafael"]] def pluck(*keys) if keys.many? map { |element| keys.map { |key| element[key] } } @@ -115,9 +137,14 @@ end # and fall back to the compatible implementation, but that's much slower than # just calling the compat method in the first place. if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false) - class Array - alias :orig_sum :sum + # Using Refinements here in order not to expose our internal method + using Module.new { + refine Array do + alias :orig_sum :sum + end + } + class Array def sum(init = nil, &block) #:nodoc: if init.is_a?(Numeric) || first.is_a?(Numeric) init ||= 0 diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb index 78b3387c3b..e357284be0 100644 --- a/activesupport/lib/active_support/core_ext/hash/compact.rb +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -1,23 +1,27 @@ class Hash - # Returns a hash with non +nil+ values. - # - # hash = { a: true, b: false, c: nil } - # hash.compact # => { a: true, b: false } - # hash # => { a: true, b: false, c: nil } - # { c: nil }.compact # => {} - # { c: true }.compact # => { c: true } - def compact - select { |_, value| !value.nil? } + unless Hash.instance_methods(false).include?(:compact) + # Returns a hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil } + # hash.compact # => { a: true, b: false } + # hash # => { a: true, b: false, c: nil } + # { c: nil }.compact # => {} + # { c: true }.compact # => { c: true } + def compact + select { |_, value| !value.nil? } + end end - # Replaces current hash with non +nil+ values. - # Returns nil if no changes were made, otherwise returns the hash. - # - # hash = { a: true, b: false, c: nil } - # hash.compact! # => { a: true, b: false } - # hash # => { a: true, b: false } - # { c: true }.compact! # => nil - def compact! - reject! { |_, value| value.nil? } + unless Hash.instance_methods(false).include?(:compact!) + # Replaces current hash with non +nil+ values. + # Returns +nil+ if no changes were made, otherwise returns the hash. + # + # hash = { a: true, b: false, c: nil } + # hash.compact! # => { a: true, b: false } + # hash # => { a: true, b: false } + # { c: true }.compact! # => nil + def compact! + reject! { |_, value| value.nil? } + end end end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 3cd96ccb9a..2a58a7f1ca 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -160,7 +160,7 @@ module ActiveSupport def normalize_keys(params) case params when Hash - Hash[params.map { |k,v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ] + Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ] when Array params.map { |v| normalize_keys(v) } else @@ -187,7 +187,7 @@ module ActiveSupport end if become_array?(value) - _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) }) + _, entries = Array.wrap(value.detect { |k, v| not v.is_a?(String) }) if entries.nil? || value["__content__"].try(:empty?) [] else @@ -206,7 +206,7 @@ module ActiveSupport elsif become_empty_string?(value) "" elsif become_hash?(value) - xml_value = Hash[value.map { |k,v| [k, deep_to_h(v)] }] + xml_value = Hash[value.map { |k, v| [k, deep_to_h(v)] }] # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with # how multipart uploaded files from HTML appear diff --git a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb index db3e7508e7..061c959442 100644 --- a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb @@ -12,11 +12,13 @@ class Hash def reverse_merge(other_hash) other_hash.merge(self) end + alias_method :with_defaults, :reverse_merge # Destructive +reverse_merge+. def reverse_merge!(other_hash) # right wins if there is no left - merge!( other_hash ) { |key,left,right| left } + merge!(other_hash) { |key, left, right| left } end alias_method :reverse_update, :reverse_merge! + alias_method :with_defaults!, :reverse_merge! end diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index 7d507ac998..2f693bff0c 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -16,7 +16,7 @@ class Hash result[key] = yield(value) end result - end + end unless method_defined? :transform_values # Destructively converts all values using the +block+ operations. # Same as +transform_values+ but modifies +self+. @@ -25,5 +25,6 @@ class Hash each do |key, value| self[key] = yield(value) end - end + end unless method_defined? :transform_values! + # TODO: Remove this file when supporting only Ruby 2.4+. end diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 4a64872392..74baae3639 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -18,12 +18,12 @@ class Integer # # equivalent to Time.now.advance(months: 4, years: 5) # (4.months + 5.years).from_now def months - ActiveSupport::Duration.new(self * 30.days, [[:months, self]]) + ActiveSupport::Duration.months(self) end alias :month :months def years - ActiveSupport::Duration.new(self * 365.25.days.to_i, [[:years, self]]) + ActiveSupport::Duration.years(self) end alias :year :years end diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb deleted file mode 100644 index dccfa3e9bb..0000000000 --- a/activesupport/lib/active_support/core_ext/kernel/debugger.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "active_support/deprecation" - -ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index d0197af95f..c02618d5f3 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,7 +1,7 @@ module Kernel module_function - # Sets $VERBOSE to nil for the duration of the block and back to its original + # Sets $VERBOSE to +nil+ for the duration of the block and back to its original # value afterwards. # # silence_warnings do diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index cd00d1b662..d273487010 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -1,6 +1,3 @@ -require "active_support/deprecation" -require "active_support/deprecation/proxy_wrappers" - class LoadError REGEXPS = [ /^no such file to load -- (.+)$/i, @@ -9,23 +6,9 @@ class LoadError /^cannot load such file -- (.+)$/i, ] - unless method_defined?(:path) - # Returns the path which was unable to be loaded. - def path - @path ||= begin - REGEXPS.find do |regex| - message =~ regex - end - $1 - end - end - end - # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) location.sub(/\.rb$/, "".freeze) == path.sub(/\.rb$/, "".freeze) end end - -MissingSourceFile = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("MissingSourceFile", "LoadError") diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index edfc8296fe..bba2b3be2e 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,7 +1,7 @@ module ActiveSupport module MarshalWithAutoloading # :nodoc: - def load(source) - super(source) + def load(source, proc = nil) + super(source, proc) rescue ArgumentError, NameError => exc if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|) # try loading the class/module diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index 57feea69a5..2930255557 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -9,4 +9,3 @@ require "active_support/core_ext/module/concerning" require "active_support/core_ext/module/delegation" require "active_support/core_ext/module/deprecation" require "active_support/core_ext/module/remove_method" -require "active_support/core_ext/module/qualified_const" diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 4a04bdd446..c48bd3354a 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -1,52 +1,4 @@ class Module - # NOTE: This method is deprecated. Please use <tt>Module#prepend</tt> that - # comes with Ruby 2.0 or newer instead. - # - # Encapsulates the common pattern of: - # - # alias_method :foo_without_feature, :foo - # alias_method :foo, :foo_with_feature - # - # With this, you simply do: - # - # alias_method_chain :foo, :feature - # - # And both aliases are set up for you. - # - # Query and bang methods (foo?, foo!) keep the same punctuation: - # - # alias_method_chain :foo?, :feature - # - # is equivalent to - # - # alias_method :foo_without_feature?, :foo? - # alias_method :foo?, :foo_with_feature? - # - # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. - def alias_method_chain(target, feature) - ActiveSupport::Deprecation.warn("alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super.") - - # Strip out punctuation on predicates, bang or writer methods since - # e.g. target?_without_feature is not a valid method name. - aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ""), $1 - yield(aliased_target, punctuation) if block_given? - - with_method = "#{aliased_target}_with_#{feature}#{punctuation}" - without_method = "#{aliased_target}_without_#{feature}#{punctuation}" - - alias_method without_method, target - alias_method target, with_method - - case - when public_method_defined?(without_method) - public target - when protected_method_defined?(without_method) - protected target - when private_method_defined?(without_method) - private target - end - end - # Allows you to make aliases for attributes, which includes # getter, setter, and a predicate. # diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 90989f4f3d..2c24081eb9 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -7,7 +7,8 @@ require "active_support/core_ext/regexp" class Module # Defines a class attribute and creates a class and instance reader methods. # The underlying class variable is set to +nil+, if it is not previously - # defined. + # defined. All class and instance methods created will be public, even if + # this method is called with a private or protected access modifier. # # module HairColors # mattr_reader :hair_colors @@ -76,7 +77,9 @@ class Module alias :cattr_reader :mattr_reader # Defines a class attribute and creates a class and instance writer methods to - # allow assignment to the attribute. + # allow assignment to the attribute. All class and instance methods created + # will be public, even if this method is called with a private or protected + # access modifier. # # module HairColors # mattr_writer :hair_colors @@ -142,6 +145,8 @@ class Module alias :cattr_writer :mattr_writer # Defines both class and instance accessors for class attributes. + # All class and instance methods created will be public, even if + # this method is called with a private or protected access modifier. # # module HairColors # mattr_accessor :hair_colors diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb index b1e6fe71e0..1e82b4acc2 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -34,7 +34,7 @@ class Module # end # # Current.new.user # => NoMethodError - def thread_mattr_reader(*syms) + def thread_mattr_reader(*syms) # :nodoc: options = syms.extract_options! syms.each do |sym| @@ -77,7 +77,7 @@ class Module # end # # Current.new.user = "DHH" # => NoMethodError - def thread_mattr_writer(*syms) + def thread_mattr_writer(*syms) # :nodoc: options = syms.extract_options! syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index f5f4ba61b7..13f3894e6c 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -6,11 +6,12 @@ class Module # option is not used. class DelegationError < NoMethodError; end + RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do + else elsif END end ensure false for if in module next nil not or redo rescue retry + return self super then true undef unless until when while yield) + DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block) DELEGATION_RESERVED_METHOD_NAMES = Set.new( - %w(_ arg args alias and BEGIN begin block break case class def defined? do - else elsif END end ensure false for if in module next nil not or redo - rescue retry return self super then true undef unless until when while - yield) + RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS ).freeze # Provides a +delegate+ class method to easily expose contained objects' @@ -19,7 +20,8 @@ class Module # ==== Options # * <tt>:to</tt> - Specifies the target object # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix - # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised + # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+ + # from being raised # # The macro receives one or more method names (specified as symbols or # strings) and the name of the target object via the <tt>:to</tt> option @@ -111,18 +113,19 @@ class Module # invoice.customer_address # => 'Vimmersvej 13' # # If the target is +nil+ and does not respond to the delegated method a - # +NoMethodError+ 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, 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. # # class User < ActiveRecord::Base # has_one :profile # delegate :age, to: :profile # end # - # User.new.age # raises NoMethodError: undefined method `age' + # User.new.age + # # => Module::DelegationError: User#age delegated to profile.age, but profile is nil # # But if not having a profile yet is fine and should not be an error # condition: @@ -216,55 +219,53 @@ class Module # When building decorators, a common pattern may emerge: # # class Partition - # def initialize(first_event) - # @events = [ first_event ] + # def initialize(event) + # @event = event # end # - # def people - # if @events.first.detail.people.any? - # @events.collect { |e| Array(e.detail.people) }.flatten.uniq - # else - # @events.collect(&:creator).uniq - # end + # def person + # @event.detail.person || @event.creator # end # # private # def respond_to_missing?(name, include_private = false) - # @events.respond_to?(name, include_private) + # @event.respond_to?(name, include_private) # end # # def method_missing(method, *args, &block) - # @events.send(method, *args, &block) + # @event.send(method, *args, &block) # end # end # - # With `Module#delegate_missing_to`, the above is condensed to: + # With <tt>Module#delegate_missing_to</tt>, the above is condensed to: # # class Partition - # delegate_missing_to :@events + # delegate_missing_to :@event # - # def initialize(first_event) - # @events = [ first_event ] + # def initialize(event) + # @event = event # end # - # def people - # if @events.first.detail.people.any? - # @events.collect { |e| Array(e.detail.people) }.flatten.uniq - # else - # @events.collect(&:creator).uniq - # end + # def person + # @event.detail.person || @event.creator # end # end # - # The target can be anything callable within the object. E.g. instance - # variables, methods, constants ant the likes. + # The target can be anything callable within the object, e.g. instance + # variables, methods, constants, etc. + # + # The delegated method must be public on the target, otherwise it will + # raise +NoMethodError+. def delegate_missing_to(target) target = target.to_s target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def respond_to_missing?(name, include_private = false) - #{target}.respond_to?(name, include_private) + # It may look like an oversight, but we deliberately do not pass + # +include_private+, because they do not get delegated. + + #{target}.respond_to?(name) || super end def method_missing(method, *args, &block) diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index 4f854a718b..ca20a6d4c5 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -5,10 +5,12 @@ class Module # # M::N.parent_name # => "M" def parent_name - if defined? @parent_name + if defined?(@parent_name) @parent_name else - @parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil + parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil + @parent_name = parent_name unless frozen? + parent_name end end @@ -55,12 +57,4 @@ class Module parents << Object unless parents.include? Object parents end - - def local_constants #:nodoc: - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Module#local_constants is deprecated and will be removed in Rails 5.1. - Use Module#constants(false) instead. - MSG - constants(false) - end end diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb deleted file mode 100644 index dccfa3e9bb..0000000000 --- a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "active_support/deprecation" - -ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/module/qualified_const.rb b/activesupport/lib/active_support/core_ext/module/qualified_const.rb deleted file mode 100644 index 62f0687ae9..0000000000 --- a/activesupport/lib/active_support/core_ext/module/qualified_const.rb +++ /dev/null @@ -1,70 +0,0 @@ -require "active_support/core_ext/string/inflections" - -#-- -# Allows code reuse in the methods below without polluting Module. -#++ - -module ActiveSupport - module QualifiedConstUtils - def self.raise_if_absolute(path) - raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/ - end - - def self.names(path) - path.split("::") - end - end -end - -## -# Extends the API for constants to be able to deal with qualified names. Arguments -# are assumed to be relative to the receiver. -# -#-- -# Qualified names are required to be relative because we are extending existing -# methods that expect constant names, ie, relative paths of length 1. For example, -# Object.const_get('::String') raises NameError and so does qualified_const_get. -#++ -class Module - def qualified_const_defined?(path, search_parents=true) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Module#qualified_const_defined? is deprecated in favour of the builtin - Module#const_defined? and will be removed in Rails 5.1. - MESSAGE - - ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) - - ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name| - return unless mod.const_defined?(name, search_parents) - mod.const_get(name) - end - return true - end - - def qualified_const_get(path) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Module#qualified_const_get is deprecated in favour of the builtin - Module#const_get and will be removed in Rails 5.1. - MESSAGE - - ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) - - ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name| - mod.const_get(name) - end - end - - def qualified_const_set(path, value) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Module#qualified_const_set is deprecated in favour of the builtin - Module#const_set and will be removed in Rails 5.1. - MESSAGE - - ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) - - const_name = path.demodulize - mod_name = path.deconstantize - mod = mod_name.empty? ? self : const_get(mod_name) - mod.const_set(const_name, value) - end -end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 5ac312790d..4f6621693e 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -99,42 +99,36 @@ module ActiveSupport::NumericWithFormat # 1234567.to_s(:human, precision: 1, # separator: ',', # significant: false) # => "1,2 Million" - def to_s(*args) - format, options = args - options ||= {} - + def to_s(format = nil, options = nil) case format + when nil + super() + when Integer, String + super(format) when :phone - return ActiveSupport::NumberHelper.number_to_phone(self, options) + return ActiveSupport::NumberHelper.number_to_phone(self, options || {}) when :currency - return ActiveSupport::NumberHelper.number_to_currency(self, options) + return ActiveSupport::NumberHelper.number_to_currency(self, options || {}) when :percentage - return ActiveSupport::NumberHelper.number_to_percentage(self, options) + return ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) when :delimited - return ActiveSupport::NumberHelper.number_to_delimited(self, options) + return ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) when :rounded - return ActiveSupport::NumberHelper.number_to_rounded(self, options) + return ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) when :human - return ActiveSupport::NumberHelper.number_to_human(self, options) + return ActiveSupport::NumberHelper.number_to_human(self, options || {}) when :human_size - return ActiveSupport::NumberHelper.number_to_human_size(self, options) + return ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) + when Symbol + super() else - if is_a?(Float) || format.is_a?(Symbol) - super() - else - super - end + super(format) end end - - def to_formatted_s(*args) - to_s(*args) - end - deprecate to_formatted_s: :to_s end # Ruby 2.4+ unifies Fixnum & Bignum into Integer. -if Integer == Fixnum +if 0.class == Integer Integer.prepend ActiveSupport::NumericWithFormat else Fixnum.prepend ActiveSupport::NumericWithFormat diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 809dfd4e07..2e6c70d418 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -19,7 +19,7 @@ class Numeric # # equivalent to Time.current.advance(months: 4, years: 5) # (4.months + 5.years).from_now def seconds - ActiveSupport::Duration.new(self, [[:seconds, self]]) + ActiveSupport::Duration.seconds(self) end alias :second :seconds @@ -27,7 +27,7 @@ class Numeric # # 2.minutes # => 2 minutes def minutes - ActiveSupport::Duration.new(self * 60, [[:minutes, self]]) + ActiveSupport::Duration.minutes(self) end alias :minute :minutes @@ -35,7 +35,7 @@ class Numeric # # 2.hours # => 2 hours def hours - ActiveSupport::Duration.new(self * 3600, [[:hours, self]]) + ActiveSupport::Duration.hours(self) end alias :hour :hours @@ -43,7 +43,7 @@ class Numeric # # 2.days # => 2 days def days - ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) + ActiveSupport::Duration.days(self) end alias :day :days @@ -51,7 +51,7 @@ class Numeric # # 2.weeks # => 2 weeks def weeks - ActiveSupport::Duration.new(self * 7.days, [[:weeks, self]]) + ActiveSupport::Duration.weeks(self) end alias :week :weeks @@ -59,7 +59,7 @@ class Numeric # # 2.fortnights # => 4 weeks def fortnights - ActiveSupport::Duration.new(self * 2.weeks, [[:weeks, self * 2]]) + ActiveSupport::Duration.weeks(self * 2) end alias :fortnight :fortnights diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index aa2282cb7e..b028df97ee 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -1,7 +1,7 @@ #-- -# Most objects are cloneable, but not all. For example you can't dup +nil+: +# Most objects are cloneable, but not all. For example you can't dup methods: # -# nil.dup # => TypeError: can't dup NilClass +# method(:puts).dup # => TypeError: allocator undefined for Method # # Classes may signal their instances are not duplicable removing +dup+/+clone+ # or raising exceptions from them. So, to dup an arbitrary object you normally @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, number, method objects; + # False for method objects; # true otherwise. def duplicable? true @@ -27,52 +27,78 @@ class Object end class NilClass - # +nil+ is not duplicable: - # - # nil.duplicable? # => false - # nil.dup # => TypeError: can't dup NilClass - def duplicable? - false + begin + nil.dup + rescue TypeError + + # +nil+ is not duplicable: + # + # nil.duplicable? # => false + # nil.dup # => TypeError: can't dup NilClass + def duplicable? + false + end end end class FalseClass - # +false+ is not duplicable: - # - # false.duplicable? # => false - # false.dup # => TypeError: can't dup FalseClass - def duplicable? - false + begin + false.dup + rescue TypeError + + # +false+ is not duplicable: + # + # false.duplicable? # => false + # false.dup # => TypeError: can't dup FalseClass + def duplicable? + false + end end end class TrueClass - # +true+ is not duplicable: - # - # true.duplicable? # => false - # true.dup # => TypeError: can't dup TrueClass - def duplicable? - false + begin + true.dup + rescue TypeError + + # +true+ is not duplicable: + # + # true.duplicable? # => false + # true.dup # => TypeError: can't dup TrueClass + def duplicable? + false + end end end class Symbol - # Symbols are not duplicable: - # - # :my_symbol.duplicable? # => false - # :my_symbol.dup # => TypeError: can't dup Symbol - def duplicable? - false + begin + :symbol.dup # Ruby 2.4.x. + "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. + rescue TypeError + + # Symbols are not duplicable: + # + # :my_symbol.duplicable? # => false + # :my_symbol.dup # => TypeError: can't dup Symbol + def duplicable? + false + end end end class Numeric - # Numbers are not duplicable: - # - # 3.duplicable? # => false - # 3.dup # => TypeError: can't dup Integer - def duplicable? - false + begin + 1.dup + rescue TypeError + + # Numbers are not duplicable: + # + # 3.duplicable? # => false + # 3.dup # => TypeError: can't dup Integer + def duplicable? + false + end end end @@ -80,8 +106,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.new("1.2").duplicable? # => true + # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)> def duplicable? true end @@ -96,3 +122,33 @@ class Method false end end + +class Complex + begin + Complex(1).dup + rescue TypeError + + # Complexes are not duplicable: + # + # Complex(1).duplicable? # => false + # Complex(1).dup # => TypeError: can't copy Complex + def duplicable? + false + end + end +end + +class Rational + begin + Rational(1).dup + rescue TypeError + + # Rationals are not duplicable: + # + # Rational(1).duplicable? # => false + # Rational(1).dup # => TypeError: can't copy Rational + def duplicable? + false + end + end +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 cf39b1d312..3a44e08630 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -62,6 +62,17 @@ class Object # # 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: + # + # class Phone < ActiveRecord::Base + # enum phone_number_type: [home: 0, office: 1, mobile: 2] + # + # with_options presence: true do + # validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys } + # end + # end + # def with_options(options, &block) option_merger = ActiveSupport::OptionMerger.new(self, options) block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) diff --git a/activesupport/lib/active_support/core_ext/regexp.rb b/activesupport/lib/active_support/core_ext/regexp.rb index 062d568228..d77d01bf42 100644 --- a/activesupport/lib/active_support/core_ext/regexp.rb +++ b/activesupport/lib/active_support/core_ext/regexp.rb @@ -3,7 +3,7 @@ class Regexp #:nodoc: options & MULTILINE == MULTILINE end - def match?(string, pos=0) + def match?(string, pos = 0) !!match(string, pos) end unless //.respond_to?(:match?) end diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index 92392d1a92..a57685bea1 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -1,12 +1,12 @@ require "securerandom" module SecureRandom - BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] + BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] # SecureRandom.base58 generates a random base58 string. # # The argument _n_ specifies the length, of the random string to be generated. # - # If _n_ is not specified or is nil, 16 is assumed. It may be larger in the future. + # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. # # The result may contain alphanumeric characters except 0, O, I and l # diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index caa48e34c5..6133826f37 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -3,7 +3,7 @@ class String # position. The first character of the string is at position 0, the next at # position 1, and so on. If a range is supplied, a substring containing # characters at offsets given by the range is returned. In both cases, if an - # offset is negative, it is counted from the end of the string. Returns nil + # offset is negative, it is counted from the end of the string. Returns +nil+ # if the initial offset falls outside the string. Returns an empty string if # the beginning of the range is greater than the end of the string. # @@ -17,7 +17,7 @@ class String # # If a Regexp is given, the matching portion of the string is returned. # If a String is given, that given string is returned if it occurs in - # the string. In both cases, nil is returned if there is no match. + # the string. In both cases, +nil+ is returned if there is no match. # # str = "hello" # str.at(/lo/) # => "lo" diff --git a/activesupport/lib/active_support/core_ext/string/indent.rb b/activesupport/lib/active_support/core_ext/string/indent.rb index e87f72fdbd..d7b58301d3 100644 --- a/activesupport/lib/active_support/core_ext/string/indent.rb +++ b/activesupport/lib/active_support/core_ext/string/indent.rb @@ -2,7 +2,7 @@ class String # Same as +indent+, except it indents the receiver in-place. # # Returns the indented string, or +nil+ if there was nothing to indent. - def indent!(amount, indent_string=nil, indent_empty_lines=false) + def indent!(amount, indent_string = nil, indent_empty_lines = false) indent_string = indent_string || self[/^[ \t]/] || " " re = indent_empty_lines ? /^/ : /^(?!$)/ gsub!(re, indent_string * amount) @@ -37,7 +37,7 @@ class String # "foo\n\nbar".indent(2) # => " foo\n\n bar" # "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar" # - def indent(amount, indent_string=nil, indent_empty_lines=false) + def indent(amount, indent_string = nil, indent_empty_lines = false) dup.tap { |_| _.indent!(amount, indent_string, indent_empty_lines) } end end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 7e12700c8c..b27cfe53f4 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -67,7 +67,7 @@ class String end # +safe_constantize+ tries to find a declared constant with the name specified - # in the string. It returns nil when the name is not in CamelCase + # in the string. It returns +nil+ when the name is not in CamelCase # or is not initialized. See ActiveSupport::Inflector.safe_constantize # # 'Module'.safe_constantize # => Module @@ -100,12 +100,17 @@ class String # a nicer looking title. +titleize+ is meant for creating pretty output. It is not # used in the Rails internals. # + # The trailing '_id','Id'.. can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true. + # By default, this parameter is false. + # # +titleize+ is also aliased as +titlecase+. # - # 'man from the boondocks'.titleize # => "Man From The Boondocks" - # 'x-men: the last stand'.titleize # => "X Men: The Last Stand" - def titleize - ActiveSupport::Inflector.titleize(self) + # 'man from the boondocks'.titleize # => "Man From The Boondocks" + # 'x-men: the last stand'.titleize # => "X Men: The Last Stand" + # 'string_ending_with_id'.titleize(keep_id_suffix: true) # => "String Ending With Id" + def titleize(keep_id_suffix: false) + ActiveSupport::Inflector.titleize(self, keep_id_suffix: keep_id_suffix) end alias_method :titlecase, :titleize @@ -128,10 +133,10 @@ class String # Removes the module part from the constant expression in the string. # - # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" - # 'Inflections'.demodulize # => "Inflections" - # '::Inflections'.demodulize # => "Inflections" - # ''.demodulize # => '' + # 'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections" + # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => '' # # See also +deconstantize+. def demodulize @@ -178,11 +183,7 @@ class String # # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a> - def parameterize(sep = :unused, separator: "-", preserve_case: false) - unless sep == :unused - ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") - separator = sep - end + def parameterize(separator: "-", preserve_case: false) ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case) end @@ -206,7 +207,7 @@ class String ActiveSupport::Inflector.classify(self) end - # Capitalizes the first word, turns underscores into spaces, and strips a + # Capitalizes the first word, turns underscores into spaces, and (by default)strips a # trailing '_id' if present. # Like +titleize+, this is meant for creating pretty output. # @@ -214,12 +215,17 @@ class String # optional parameter +capitalize+ to false. # By default, this parameter is true. # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - # 'author_id'.humanize(capitalize: false) # => "author" - # '_id'.humanize # => "Id" - def humanize(options = {}) - ActiveSupport::Inflector.humanize(self, options) + # The trailing '_id' can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true. + # By default, this parameter is false. + # + # 'employee_salary'.humanize # => "Employee salary" + # 'author_id'.humanize # => "Author" + # 'author_id'.humanize(capitalize: false) # => "author" + # '_id'.humanize # => "Id" + # 'author_id'.humanize(keep_id_suffix: true) # => "Author Id" + def humanize(capitalize: true, keep_id_suffix: false) + ActiveSupport::Inflector.humanize(self, capitalize: capitalize, keep_id_suffix: keep_id_suffix) end # Converts just the first character to uppercase. 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 2b7e556ced..94ce3f6a61 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -152,18 +152,16 @@ module ActiveSupport #:nodoc: def [](*args) if args.size < 2 super - else - if html_safe? - new_safe_buffer = super - - if new_safe_buffer - new_safe_buffer.instance_variable_set :@html_safe, true - end + elsif html_safe? + new_safe_buffer = super - new_safe_buffer - else - to_str[*args] + if new_safe_buffer + new_safe_buffer.instance_variable_set :@html_safe, true end + + new_safe_buffer + else + to_str[*args] end end @@ -202,7 +200,7 @@ module ActiveSupport #:nodoc: def %(args) case args when Hash - escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }] + escaped_args = Hash[args.map { |k, arg| [k, html_escape_interpolated_argument(arg)] }] else escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) } end diff --git a/activesupport/lib/active_support/core_ext/struct.rb b/activesupport/lib/active_support/core_ext/struct.rb deleted file mode 100644 index dccfa3e9bb..0000000000 --- a/activesupport/lib/active_support/core_ext/struct.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "active_support/deprecation" - -ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 2cbfa14772..d3f23f4663 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -53,6 +53,29 @@ class Time end alias_method :at_without_coercion, :at alias_method :at, :at_with_coercion + + # Creates a +Time+ instance from an RFC 3339 string. + # + # Time.rfc3339('1999-12-31T14:00:00-10:00') # => 2000-01-01 00:00:00 -1000 + # + # If the time or offset components are missing then an +ArgumentError+ will be raised. + # + # Time.rfc3339('1999-12-31') # => ArgumentError: invalid date + def rfc3339(str) + parts = Date._rfc3339(str) + + raise ArgumentError, "invalid date" if parts.empty? + + Time.new( + parts.fetch(:year), + parts.fetch(:mon), + parts.fetch(:mday), + parts.fetch(:hour), + parts.fetch(:min), + parts.fetch(:sec) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset) + ) + end end # Returns the number of seconds since 00:00:00. @@ -84,36 +107,42 @@ class Time # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour - # and minute is passed, then sec, usec and nsec is set to 0. The +options+ - # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, - # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> - # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter + # takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, + # <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>, + # <tt>:offset</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0) def change(options) - new_year = options.fetch(:year, year) - new_month = options.fetch(:month, month) - new_day = options.fetch(:day, day) - new_hour = options.fetch(:hour, hour) - new_min = options.fetch(:min, options[:hour] ? 0 : min) - new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_year = options.fetch(:year, year) + new_month = options.fetch(:month, month) + new_day = options.fetch(:day, day) + new_hour = options.fetch(:hour, hour) + new_min = options.fetch(:min, options[:hour] ? 0 : min) + new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_offset = options.fetch(:offset, nil) if new_nsec = options[:nsec] raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] new_usec = Rational(new_nsec, 1000) else - new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) end - if utc? - ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) + raise ArgumentError, "argument out of range" if new_usec >= 1000000 + + new_sec += Rational(new_usec, 1000000) + + if new_offset + ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset) + elsif utc? + ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec) elsif zone - ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) + ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec) else - raise ArgumentError, "argument out of range" if new_usec >= 1000000 - ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) + ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset) end end diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb index ca4b9574d5..45e86b77ce 100644 --- a/activesupport/lib/active_support/core_ext/time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb @@ -1,5 +1,14 @@ require "active_support/core_ext/date_and_time/compatibility" +require "active_support/core_ext/module/remove_method" class Time - prepend DateAndTime::Compatibility + include DateAndTime::Compatibility + + remove_possible_method :to_time + + # Either return +self+ or the time in the local system timezone depending + # on the setting of +ActiveSupport.to_time_preserves_timezone+. + def to_time + preserve_timezone ? self : getlocal + end end diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index f2bbe55aa6..595bda6b4f 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -64,4 +64,7 @@ class Time def formatted_offset(colon = true, alternate_utc_string = nil) utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon) end + + # Aliased to +xmlschema+ for compatibility with +DateTime+ + alias_method :rfc3339, :xmlschema end diff --git a/activesupport/lib/active_support/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb deleted file mode 100644 index d095d76ebb..0000000000 --- a/activesupport/lib/active_support/core_ext/time/marshal.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "active_support/deprecation" - -ActiveSupport::Deprecation.warn("This is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb new file mode 100644 index 0000000000..872b0663c7 --- /dev/null +++ b/activesupport/lib/active_support/current_attributes.rb @@ -0,0 +1,193 @@ +module ActiveSupport + # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically + # before and after each request. This allows you to keep all the per-request attributes easily + # available to the whole system. + # + # The following full app-like example demonstrates how to use a Current class to + # facilitate easy access to the global, per-request attributes without passing them deeply + # around everywhere: + # + # # app/models/current.rb + # class Current < ActiveSupport::CurrentAttributes + # attribute :account, :user + # attribute :request_id, :user_agent, :ip_address + # + # resets { Time.zone = nil } + # + # def user=(user) + # super + # self.account = user.account + # Time.zone = user.time_zone + # end + # end + # + # # app/controllers/concerns/authentication.rb + # module Authentication + # extend ActiveSupport::Concern + # + # included do + # before_action :authenticate + # end + # + # private + # def authenticate + # if authenticated_user = User.find_by(id: cookies.signed[:user_id]) + # Current.user = authenticated_user + # else + # redirect_to new_session_url + # end + # end + # end + # + # # app/controllers/concerns/set_current_request_details.rb + # module SetCurrentRequestDetails + # extend ActiveSupport::Concern + # + # included do + # before_action do + # Current.request_id = request.uuid + # Current.user_agent = request.user_agent + # Current.ip_address = request.ip + # end + # end + # end + # + # class ApplicationController < ActionController::Base + # include Authentication + # include SetCurrentRequestDetails + # end + # + # class MessagesController < ApplicationController + # def create + # Current.account.messages.create(message_params) + # end + # end + # + # class Message < ApplicationRecord + # belongs_to :creator, default: -> { Current.user } + # after_create { |message| Event.create(record: message) } + # end + # + # class Event < ApplicationRecord + # before_create do + # self.request_id = Current.request_id + # self.user_agent = Current.user_agent + # self.ip_address = Current.ip_address + # end + # end + # + # A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result. + # Current should only be used for a few, top-level globals, like account, user, and request details. + # The attributes stuck in Current should be used by more or less all actions on all requests. If you start + # sticking controller-specific attributes in there, you're going to create a mess. + class CurrentAttributes + include ActiveSupport::Callbacks + define_callbacks :reset + + class << self + # Returns singleton instance for this class in this thread. If none exists, one is created. + def instance + current_instances[name] ||= new + end + + # Declares one or more attributes that will be given both class and instance accessor methods. + def attribute(*names) + generated_attribute_methods.module_eval do + names.each do |name| + define_method(name) do + attributes[name.to_sym] + end + + define_method("#{name}=") do |attribute| + attributes[name.to_sym] = attribute + end + end + end + + names.each do |name| + define_singleton_method(name) do + instance.public_send(name) + end + + define_singleton_method("#{name}=") do |attribute| + instance.public_send("#{name}=", attribute) + end + end + end + + # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone. + def resets(&block) + set_callback :reset, :after, &block + end + + delegate :set, :reset, to: :instance + + def reset_all # :nodoc: + current_instances.each_value(&:reset) + end + + def clear_all # :nodoc: + reset_all + current_instances.clear + end + + private + def generated_attribute_methods + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } + end + + def current_instances + Thread.current[:current_attributes_instances] ||= {} + end + + def method_missing(name, *args, &block) + # Caches the method definition as a singleton method of the receiver. + # + # By letting #delegate handle it, we avoid an enclosure that'll capture args. + singleton_class.delegate name, to: :instance + + send(name, *args, &block) + end + end + + attr_accessor :attributes + + def initialize + @attributes = {} + end + + # Expose one or more attributes within a block. Old values are returned after the block concludes. + # Example demonstrating the common use of needing to set Current attributes outside the request-cycle: + # + # class Chat::PublicationJob < ApplicationJob + # def perform(attributes, room_number, creator) + # Current.set(person: creator) do + # Chat::Publisher.publish(attributes: attributes, room_number: room_number) + # end + # end + # end + def set(set_attributes) + old_attributes = compute_attributes(set_attributes.keys) + assign_attributes(set_attributes) + yield + ensure + assign_attributes(old_attributes) + end + + # Reset all attributes. Should be called before and after actions, when used as a per-request singleton. + def reset + run_callbacks :reset do + self.attributes = {} + end + end + + private + def assign_attributes(new_attributes) + new_attributes.each { |key, value| public_send("#{key}=", value) } + end + + def compute_attributes(keys) + keys.collect { |key| [ key, public_send(key) ] }.to_h + end + end +end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 216d52164e..e125b657f2 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -6,7 +6,6 @@ require "active_support/core_ext/module/aliasing" require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/introspection" require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/module/qualified_const" require "active_support/core_ext/object/blank" require "active_support/core_ext/kernel/reporting" require "active_support/core_ext/load_error" @@ -108,7 +107,7 @@ module ActiveSupport #:nodoc: def initialize @watching = [] - @stack = Hash.new { |h,k| h[k] = [] } + @stack = Hash.new { |h, k| h[k] = [] } end def each(&block) @@ -243,7 +242,7 @@ module ActiveSupport #:nodoc: # resolution deterministic for constants with the same relative name in # different namespaces whose evaluation would depend on load order # otherwise. - def require_dependency(file_name, message = "No such file to load -- %s") + def require_dependency(file_name, message = "No such file to load -- %s.rb") file_name = file_name.to_path if file_name.respond_to?(:to_path) unless file_name.is_a?(String) raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}" diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 191e582de8..35cddcfde6 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -15,6 +15,7 @@ module ActiveSupport require "active_support/deprecation/instance_delegator" require "active_support/deprecation/behaviors" require "active_support/deprecation/reporting" + require "active_support/deprecation/constant_accessor" require "active_support/deprecation/method_wrappers" require "active_support/deprecation/proxy_wrappers" require "active_support/core_ext/module/deprecation" @@ -29,10 +30,10 @@ module ActiveSupport attr_accessor :deprecation_horizon # It accepts two parameters on initialization. The first is a version of library - # and the second is a library name + # and the second is a library name. # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = "5.2", gem_name = "Rails") + def initialize(deprecation_horizon = "5.3", 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/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 1d1354c23e..a9a182f212 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -9,18 +9,18 @@ module ActiveSupport class Deprecation # Default warning behaviors per Rails.env. DEFAULT_BEHAVIORS = { - raise: ->(message, callstack) { + raise: ->(message, callstack, deprecation_horizon, gem_name) { e = DeprecationException.new(message) e.set_backtrace(callstack.map(&:to_s)) raise e }, - stderr: ->(message, callstack) { + stderr: ->(message, callstack, deprecation_horizon, gem_name) { $stderr.puts(message) $stderr.puts callstack.join("\n ") if debug }, - log: ->(message, callstack) { + log: ->(message, callstack, deprecation_horizon, gem_name) { logger = if defined?(Rails.logger) && Rails.logger Rails.logger @@ -32,12 +32,16 @@ module ActiveSupport logger.debug callstack.join("\n ") if debug }, - notify: ->(message, callstack) { - ActiveSupport::Notifications.instrument("deprecation.rails", - message: message, callstack: callstack) + notify: ->(message, callstack, deprecation_horizon, gem_name) { + notification_name = "deprecation.#{gem_name.underscore.tr('/', '_')}" + ActiveSupport::Notifications.instrument(notification_name, + message: message, + callstack: callstack, + gem_name: gem_name, + deprecation_horizon: deprecation_horizon) }, - silence: ->(message, callstack) {}, + silence: ->(message, callstack, deprecation_horizon, gem_name) {}, } # Behavior module allows to determine how to display deprecation messages. @@ -83,8 +87,17 @@ module ActiveSupport # # custom stuff # } def behavior=(behavior) - @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b } + @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) } end + + private + def arity_coerce(behavior) + if behavior.arity == 4 || behavior.arity == -1 + behavior + else + -> message, callstack, _, _ { behavior.call(message, callstack) } + end + end end end end diff --git a/activesupport/lib/active_support/deprecation/constant_accessor.rb b/activesupport/lib/active_support/deprecation/constant_accessor.rb new file mode 100644 index 0000000000..2b19de365f --- /dev/null +++ b/activesupport/lib/active_support/deprecation/constant_accessor.rb @@ -0,0 +1,50 @@ +require "active_support/inflector/methods" + +module ActiveSupport + class Deprecation + # DeprecatedConstantAccessor transforms a constant into a deprecated one by + # hooking +const_missing+. + # + # It takes the names of an old (deprecated) constant and of a new constant + # (both in string form) and optionally a deprecator. The deprecator defaults + # to +ActiveSupport::Deprecator+ if none is specified. + # + # The deprecated constant now returns the same object as the new one rather + # than a proxy object, so it can be used transparently in +rescue+ blocks + # etc. + # + # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) + # + # (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 + # deprecate_constant 'PLANETS', 'PLANETS_POST_2006' + # + # PLANETS.map { |planet| planet.capitalize } + # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead. + # (Backtrace information…) + # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] + module DeprecatedConstantAccessor + def self.included(base) + extension = Module.new do + def const_missing(missing_const_name) + if class_variable_defined?(:@@_deprecated_constants) + if (replacement = class_variable_get(:@@_deprecated_constants)[missing_const_name.to_s]) + replacement[:deprecator].warn(replacement[:message] || "#{name}::#{missing_const_name} is deprecated! Use #{replacement[:new]} instead.", caller_locations) + return ActiveSupport::Inflector.constantize(replacement[:new].to_s) + end + end + super + end + + def deprecate_constant(const_name, new_constant, message: nil, deprecator: ActiveSupport::Deprecation.instance) + class_variable_set(:@@_deprecated_constants, {}) unless class_variable_defined?(:@@_deprecated_constants) + class_variable_get(:@@_deprecated_constants)[const_name.to_s] = { new: new_constant, message: message, deprecator: deprecator } + end + end + base.singleton_class.prepend extension + end + end + end +end diff --git a/activesupport/lib/active_support/deprecation/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb index 8efa6aabdc..6d390f3b37 100644 --- a/activesupport/lib/active_support/deprecation/instance_delegator.rb +++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb @@ -6,6 +6,7 @@ module ActiveSupport module InstanceDelegator # :nodoc: def self.included(base) base.extend(ClassMethods) + base.singleton_class.prepend(OverrideDelegators) base.public_class_method :new end @@ -19,6 +20,18 @@ module ActiveSupport singleton_class.delegate(method_name, to: :instance) end end + + module OverrideDelegators # :nodoc: + def warn(message = nil, callstack = nil) + callstack ||= caller_locations(2) + super + end + + def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil) + caller_backtrace ||= caller_locations(2) + super + end + end end end end diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index 7655fa4f99..930d71e8d2 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -18,7 +18,7 @@ module ActiveSupport # # Using the default deprecator: # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead') - # # => [:aaa, :bbb, :ccc] + # # => Fred # # Fred.aaa # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10) diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 1c6618b19a..ce39e9a232 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -122,10 +122,11 @@ module ActiveSupport # (Backtrace information…) # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy - def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) + def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.") @old_const = old_const @new_const = new_const @deprecator = deprecator + @message = message end # Returns the class of the new constant. @@ -143,7 +144,7 @@ module ActiveSupport end def warn(callstack, called, args) - @deprecator.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack) + @deprecator.warn(@message, callstack) end end end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index b8d200ba94..140bdccbb3 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -18,7 +18,7 @@ module ActiveSupport callstack ||= caller_locations(2) deprecation_message(callstack, message).tap do |m| - behavior.each { |b| b.call(m, callstack) } + behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) } end end @@ -48,11 +48,11 @@ module ActiveSupport private # Outputs a deprecation warning message # - # ActiveSupport::Deprecation.deprecated_method_warning(:method_name) + # deprecated_method_warning(:method_name) # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon}" - # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, :another_method) + # deprecated_method_warning(:method_name, :another_method) # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (use another_method instead)" - # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, "Optional message") + # deprecated_method_warning(:method_name, "Optional message") # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (Optional message)" def deprecated_method_warning(method_name, message = nil) warning = "#{method_name} is deprecated and will be removed from #{gem_name} #{deprecation_horizon}" @@ -102,7 +102,7 @@ module ActiveSupport end end - RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/" + RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) def ignored_callstack(path) path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"]) diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 6ec0c3a70a..39deb2313f 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -1,5 +1,8 @@ require "active_support/core_ext/array/conversions" +require "active_support/core_ext/module/delegation" require "active_support/core_ext/object/acts_like" +require "active_support/core_ext/string/filters" +require "active_support/deprecation" module ActiveSupport # Provides accurate date and time measurements using Date#advance and @@ -7,22 +10,206 @@ module ActiveSupport # # 1.month.ago # equivalent to Time.now.advance(months: -1) class Duration + class Scalar < Numeric #:nodoc: + attr_reader :value + delegate :to_i, :to_f, :to_s, to: :value + + def initialize(value) + @value = value + end + + def coerce(other) + [Scalar.new(other), self] + end + + def -@ + Scalar.new(-value) + end + + def <=>(other) + if Scalar === other || Duration === other + value <=> other.value + elsif Numeric === other + value <=> other + else + nil + end + end + + def +(other) + if Duration === other + seconds = value + other.parts[:seconds] + new_parts = other.parts.merge(seconds: seconds) + new_value = value + other.value + + Duration.new(new_value, new_parts) + else + calculate(:+, other) + end + end + + def -(other) + if Duration === other + seconds = value - other.parts[:seconds] + new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h + new_parts = new_parts.merge(seconds: seconds) + new_value = value - other.value + + Duration.new(new_value, new_parts) + else + calculate(:-, other) + end + end + + def *(other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h + new_value = value * other.value + + Duration.new(new_value, new_parts) + else + calculate(:*, other) + end + end + + def /(other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value / other_value] }.to_h + new_value = new_parts.inject(0) { |total, (part, value)| total + value * Duration::PARTS_IN_SECONDS[part] } + + Duration.new(new_value, new_parts) + else + calculate(:/, other) + end + end + + private + def calculate(op, other) + if Scalar === other + Scalar.new(value.public_send(op, other.value)) + elsif Numeric === other + Scalar.new(value.public_send(op, other)) + else + raise_type_error(other) + end + end + + def raise_type_error(other) + raise TypeError, "no implicit conversion of #{other.class} into #{self.class}" + end + end + + SECONDS_PER_MINUTE = 60 + SECONDS_PER_HOUR = 3600 + SECONDS_PER_DAY = 86400 + SECONDS_PER_WEEK = 604800 + SECONDS_PER_MONTH = 2629746 # 1/12 of a gregorian year + SECONDS_PER_YEAR = 31556952 # length of a gregorian year (365.2425 days) + + PARTS_IN_SECONDS = { + seconds: 1, + minutes: SECONDS_PER_MINUTE, + hours: SECONDS_PER_HOUR, + days: SECONDS_PER_DAY, + weeks: SECONDS_PER_WEEK, + months: SECONDS_PER_MONTH, + years: SECONDS_PER_YEAR + }.freeze + attr_accessor :value, :parts autoload :ISO8601Parser, "active_support/duration/iso8601_parser" autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer" + class << self + # Creates a new Duration from string formatted according to ISO 8601 Duration. + # + # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information. + # This method allows negative parts to be present in pattern. + # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+. + def parse(iso8601duration) + parts = ISO8601Parser.new(iso8601duration).parse! + new(calculate_total_seconds(parts), parts) + end + + def ===(other) #:nodoc: + other.is_a?(Duration) + rescue ::NoMethodError + false + end + + def seconds(value) #:nodoc: + new(value, [[:seconds, value]]) + end + + def minutes(value) #:nodoc: + new(value * SECONDS_PER_MINUTE, [[:minutes, value]]) + end + + def hours(value) #:nodoc: + new(value * SECONDS_PER_HOUR, [[:hours, value]]) + end + + def days(value) #:nodoc: + new(value * SECONDS_PER_DAY, [[:days, value]]) + end + + def weeks(value) #:nodoc: + new(value * SECONDS_PER_WEEK, [[:weeks, value]]) + end + + def months(value) #:nodoc: + new(value * SECONDS_PER_MONTH, [[:months, value]]) + end + + def years(value) #:nodoc: + new(value * SECONDS_PER_YEAR, [[:years, value]]) + end + + private + + def calculate_total_seconds(parts) + parts.inject(0) do |total, (part, value)| + total + value * PARTS_IN_SECONDS[part] + end + end + end + def initialize(value, parts) #:nodoc: - @value, @parts = value, parts + @value, @parts = value, parts.to_h + @parts.default = 0 + end + + def coerce(other) #:nodoc: + if Scalar === other + [other, self] + else + [Scalar.new(other), self] + end + end + + # Compares one Duration with another or a Numeric to this Duration. + # Numeric values are treated as seconds. + def <=>(other) + if Duration === other + value <=> other.value + elsif Numeric === other + value <=> other + end end # Adds another Duration or a Numeric to this Duration. Numeric values # are treated as seconds. def +(other) if Duration === other - Duration.new(value + other.value, @parts + other.parts) + parts = @parts.dup + other.parts.each do |(key, value)| + parts[key] += value + end + Duration.new(value + other.value, parts) else - Duration.new(value + other, @parts + [[:seconds, other]]) + seconds = @parts[:seconds] + other + Duration.new(value + other, @parts.merge(seconds: seconds)) end end @@ -32,8 +219,30 @@ module ActiveSupport self + (-other) end + # Multiplies this Duration by a Numeric and returns a new Duration. + def *(other) + if Scalar === other || Duration === other + Duration.new(value * other.value, parts.map { |type, number| [type, number * other.value] }) + elsif Numeric === other + Duration.new(value * other, parts.map { |type, number| [type, number * other] }) + else + raise_type_error(other) + end + end + + # Divides this Duration by a Numeric and returns a new Duration. + def /(other) + if Scalar === other || Duration === other + Duration.new(value / other.value, parts.map { |type, number| [type, number / other.value] }) + elsif Numeric === other + Duration.new(value / other, parts.map { |type, number| [type, number / other] }) + else + raise_type_error(other) + end + end + def -@ #:nodoc: - Duration.new(-value, parts.map { |type,number| [type, -number] }) + Duration.new(-value, parts.map { |type, number| [type, -number] }) end def is_a?(klass) #:nodoc: @@ -70,14 +279,14 @@ module ActiveSupport # 1.day.to_i # => 86400 # # Note that this conversion makes some assumptions about the - # duration of some periods, e.g. months are always 30 days - # and years are 365.25 days: + # duration of some periods, e.g. months are always 1/12 of year + # and years are 365.2425 days: # - # # equivalent to 30.days.to_i - # 1.month.to_i # => 2592000 + # # equivalent to (1.year / 12).to_i + # 1.month.to_i # => 2629746 # - # # equivalent to 365.25.days.to_i - # 1.year.to_i # => 31557600 + # # equivalent to 365.2425.days.to_i + # 1.year.to_i # => 31556952 # # In such cases, Ruby's core # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and @@ -97,18 +306,13 @@ module ActiveSupport @value.hash end - def self.===(other) #:nodoc: - other.is_a?(Duration) - rescue ::NoMethodError - false - end - # Calculates a new Time or Date that is as far in the future # as this Duration represents. def since(time = ::Time.current) sum(1, time) end alias :from_now :since + alias :after :since # Calculates a new Time or Date that is as far in the past # as this Duration represents. @@ -116,10 +320,11 @@ module ActiveSupport sum(-1, time) end alias :until :ago + alias :before :ago def inspect #:nodoc: parts. - reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. + reduce(::Hash.new(0)) { |h, (l, r)| h[l] += r; h }. sort_by { |unit, _ | [:years, :months, :weeks, :days, :hours, :minutes, :seconds].index(unit) }. map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }. to_sentence(locale: ::I18n.default_locale) @@ -129,33 +334,16 @@ module ActiveSupport to_i end - def respond_to_missing?(method, include_private=false) #:nodoc: - @value.respond_to?(method, include_private) - end - - # Creates a new Duration from string formatted according to ISO 8601 Duration. - # - # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information. - # This method allows negative parts to be present in pattern. - # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+. - def self.parse(iso8601duration) - parts = ISO8601Parser.new(iso8601duration).parse! - time = ::Time.current - new(time.advance(parts) - time, 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) ISO8601Serializer.new(self, precision: precision).serialize end - delegate :<=>, to: :value - - protected + private - def sum(sign, time = ::Time.current) #:nodoc: - parts.inject(time) do |t,(type,number)| + def sum(sign, time = ::Time.current) + parts.inject(time) do |t, (type, number)| if t.acts_like?(:time) || t.acts_like?(:date) if type == :seconds t.since(sign * number) @@ -172,10 +360,16 @@ module ActiveSupport end end - private + def respond_to_missing?(method, _) + value.respond_to?(method) + end + + def method_missing(method, *args, &block) + value.public_send(method, *args, &block) + end - def method_missing(method, *args, &block) #:nodoc: - value.send(method, *args, &block) + def raise_type_error(other) + raise TypeError, "no implicit conversion of #{other.class} into #{self.class}" end end end diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb index 542630b950..e5d458b3ab 100644 --- a/activesupport/lib/active_support/duration/iso8601_serializer.rb +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -4,7 +4,7 @@ require "active_support/core_ext/hash/transform_values" module ActiveSupport class Duration # Serializes duration to string according to ISO 8601 Duration format. - class ISO8601Serializer + class ISO8601Serializer # :nodoc: def initialize(duration, precision: nil) @duration = duration @precision = precision @@ -26,7 +26,7 @@ module ActiveSupport if parts.key?(:seconds) time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S" end - output << "T#{time}" if time.present? + output << "T#{time}" unless time.empty? "#{sign}#{output}" end @@ -37,7 +37,7 @@ module ActiveSupport # Zero parts are removed as not significant. # If all parts are negative it will negate all of them and return minus as a sign. def normalize - parts = @duration.parts.each_with_object(Hash.new(0)) do |(k,v),p| + parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p| p[k] += v unless v.zero? end # If all parts are negative - let's make a negative duration diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb index f54f88eb0a..f59f5d17dc 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -17,7 +17,7 @@ module ActiveSupport # # Example: # - # checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" }) + # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" } # checker.updated? # # => false # checker.execute_if_updated @@ -32,6 +32,10 @@ module ActiveSupport # class EventedFileUpdateChecker #:nodoc: all def initialize(files, dirs = {}, &block) + unless block + raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker" + end + @ph = PathHelper.new @files = files.map { |f| @ph.xpath(f) }.to_set @@ -121,6 +125,11 @@ module ActiveSupport dtw.compact! dtw.uniq! + normalized_gem_paths = Gem.path.map { |path| File.join path, "" } + dtw = dtw.reject do |path| + normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) } + end + @ph.filter_out_descendants(dtw) end diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index 4c8b03c9df..ca88e7876b 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -19,6 +19,23 @@ module ActiveSupport set_callback(:complete, *args, &block) end + RunHook = Struct.new(:hook) do # :nodoc: + def before(target) + hook_state = target.send(:hook_state) + hook_state[hook] = hook.run + end + end + + CompleteHook = Struct.new(:hook) do # :nodoc: + def before(target) + hook_state = target.send(:hook_state) + if hook_state.key?(hook) + hook.complete hook_state[hook] + end + end + alias after before + end + # Register an object to be invoked during both the +run+ and # +complete+ steps. # @@ -29,19 +46,11 @@ module ActiveSupport # invoked in that situation.) def self.register_hook(hook, outer: false) if outer - run_args = [prepend: true] - complete_args = [:after] + to_run RunHook.new(hook), prepend: true + to_complete :after, CompleteHook.new(hook) else - run_args = complete_args = [] - end - - to_run(*run_args) do - hook_state[hook] = hook.run - end - to_complete(*complete_args) do - if hook_state.key?(hook) - hook.complete hook_state[hook] - end + to_run RunHook.new(hook) + to_complete CompleteHook.new(hook) end end diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 98aceabe21..2b5e3c1350 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -38,6 +38,10 @@ module ActiveSupport # changes. The array of files and list of directories cannot be changed # after FileUpdateChecker has been initialized. def initialize(files, dirs = {}, &block) + unless block + raise ArgumentError, "A block is required to initialize a FileUpdateChecker" + end + @files = files.freeze @glob = compile_glob(dirs) @block = block @@ -145,7 +149,7 @@ module ActiveSupport end def escape(key) - key.gsub(",",'\,') + key.gsub(",", '\,') end def compile_ext(array) diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 74f2d8dd4b..371a39a5e6 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -6,7 +6,7 @@ module ActiveSupport module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb index 4aee8dddba..95a86889ec 100644 --- a/activesupport/lib/active_support/gzip.rb +++ b/activesupport/lib/active_support/gzip.rb @@ -21,11 +21,11 @@ module ActiveSupport # Decompresses a gzipped string. def self.decompress(source) - Zlib::GzipReader.new(StringIO.new(source)).read + Zlib::GzipReader.wrap(StringIO.new(source), &:read) end # Compresses a string using gzip. - def self.compress(source, level=Zlib::DEFAULT_COMPRESSION, strategy=Zlib::DEFAULT_STRATEGY) + def self.compress(source, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY) output = Stream.new gz = Zlib::GzipWriter.new(output, level, strategy) gz.write(source) diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 74a603c05d..3b185dd86b 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -40,6 +40,12 @@ module ActiveSupport # rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access # # which may be handy. + # + # To access this class outside of Rails, require the core extension with: + # + # require "active_support/core_ext/hash/indifferent_access" + # + # which will, in turn, require this file. class HashWithIndifferentAccess < Hash # Returns +true+ so that <tt>Array#extract_options!</tt> finds members of # this class. @@ -78,15 +84,6 @@ module ActiveSupport end end - def self.new_from_hash_copying_default(hash) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default` - has been deprecated, and will be removed in Rails 5.1. The behavior of - this method is now identical to the behavior of `.new`. - MSG - new(hash) - end - def self.[](*args) new.merge!(Hash[*args]) end @@ -198,6 +195,19 @@ module ActiveSupport indices.collect { |key| self[convert_key(key)] } end + # Returns an array of the values at the specified indices, but also + # raises an exception when one of the keys can't be found. + # + # hash = ActiveSupport::HashWithIndifferentAccess.new + # hash[:a] = 'x' + # hash[:b] = 'y' + # hash.fetch_values('a', 'b') # => ["x", "y"] + # hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"] + # hash.fetch_values('a', 'c') # => KeyError: key not found: "c" + def fetch_values(*indices, &block) + indices.collect { |key| fetch(key, &block) } + end if Hash.method_defined?(:fetch_values) + # Returns a shallow copy of the hash. # # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } }) @@ -228,11 +238,13 @@ module ActiveSupport def reverse_merge(other_hash) super(self.class.new(other_hash)) end + alias_method :with_defaults, :reverse_merge # Same semantics as +reverse_merge+ but modifies the receiver in-place. def reverse_merge!(other_hash) - replace(reverse_merge( other_hash )) + replace(reverse_merge(other_hash)) end + alias_method :with_defaults!, :reverse_merge! # Replaces the contents of this hash with other_hash. # @@ -267,6 +279,15 @@ module ActiveSupport dup.tap { |hash| hash.reject!(*args, &block) } end + def transform_values(*args, &block) + return to_enum(:transform_values) unless block_given? + dup.tap { |hash| hash.transform_values!(*args, &block) } + end + + def compact + dup.tap(&:compact!) + end + # Convert to a regular hash with string keys. def to_hash _new_hash = Hash.new @@ -278,12 +299,12 @@ module ActiveSupport _new_hash end - protected - def convert_key(key) + private + def convert_key(key) # :doc: key.kind_of?(Symbol) ? key.to_s : key end - def convert_value(value, options = {}) + def convert_value(value, options = {}) # :doc: if value.is_a? Hash if options[:for] == :to_hash value.to_hash @@ -300,7 +321,7 @@ module ActiveSupport end end - def set_defaults(target) + def set_defaults(target) # :doc: if default_proc target.default_proc = default_proc.dup else @@ -310,4 +331,6 @@ module ActiveSupport end end +# :stopdoc: + HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index f0408f429c..1a1f1a1257 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -10,4 +10,4 @@ end require "active_support/lazy_load_hooks" ActiveSupport.run_load_hooks(:i18n) -I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" +I18n.load_path << File.expand_path("locale/en.yml", __dir__) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 08c9ef8f02..51fe6f3418 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -2,6 +2,8 @@ require "active_support" require "active_support/file_update_checker" require "active_support/core_ext/array/wrap" +# :enddoc: + module I18n class Railtie < Rails::Railtie config.i18n = ActiveSupport::OrderedOptions.new @@ -21,8 +23,6 @@ module I18n I18n::Railtie.initialize_i18n(app) end - protected - @i18n_inited = false # Setup i18n configuration. @@ -42,7 +42,7 @@ module I18n case setting when :railties_load_path reloadable_paths = value - app.config.i18n.load_path.unshift(*value.map(&:existent).flatten) + app.config.i18n.load_path.unshift(*value.flat_map(&:existent)) when :load_path I18n.load_path += value else @@ -58,7 +58,7 @@ module I18n directories = watched_dirs_with_extensions(reloadable_paths) reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do I18n.load_path.keep_if { |p| File.exist?(p) } - I18n.load_path |= reloadable_paths.map(&:existent).flatten + I18n.load_path |= reloadable_paths.flat_map(&:existent) I18n.reload! end @@ -66,10 +66,6 @@ module I18n app.reloaders << reloader app.reloader.to_run do reloader.execute_if_updated { require_unload_lock! } - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true end reloader.execute diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index aa68f9ec9e..c47a2e34e1 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,5 +1,6 @@ require "concurrent/map" require "active_support/core_ext/array/prepend_and_append" +require "active_support/core_ext/regexp" require "active_support/i18n" module ActiveSupport @@ -43,13 +44,14 @@ module ActiveSupport end def add(words) - concat(words.flatten.map(&:downcase)) - @regex_array += map { |word| to_regex(word) } + words = words.flatten.map(&:downcase) + concat(words) + @regex_array += words.map { |word| to_regex(word) } self end def uncountable?(str) - @regex_array.any? { |regex| regex === str } + @regex_array.any? { |regex| regex.match? str } end private diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index c80243c40a..ff1a0cb8c7 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -28,7 +28,7 @@ module ActiveSupport # pluralize('CamelOctopus') # => "CamelOctopi" # pluralize('ley', :es) # => "leyes" def pluralize(word, locale = :en) - apply_inflections(word, inflections(locale).plurals) + apply_inflections(word, inflections(locale).plurals, locale) end # The reverse of #pluralize, returns the singular form of a word in a @@ -45,7 +45,7 @@ module ActiveSupport # singularize('CamelOctopi') # => "CamelOctopus" # singularize('leyes', :es) # => "ley" def singularize(word, locale = :en) - apply_inflections(word, inflections(locale).singulars) + apply_inflections(word, inflections(locale).singulars, locale) end # Converts strings to UpperCamelCase. @@ -108,33 +108,38 @@ module ActiveSupport # * Replaces underscores with spaces, if any. # * Downcases all words except acronyms. # * Capitalizes the first word. - # # The capitalization of the first word can be turned off by setting the # +:capitalize+ option to false (default is true). # - # humanize('employee_salary') # => "Employee salary" - # humanize('author_id') # => "Author" - # humanize('author_id', capitalize: false) # => "author" - # humanize('_id') # => "Id" + # The trailing '_id' can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true (default is false). + # + # humanize('employee_salary') # => "Employee salary" + # humanize('author_id') # => "Author" + # humanize('author_id', capitalize: false) # => "author" + # humanize('_id') # => "Id" + # humanize('author_id', keep_id_suffix: true) # => "Author Id" # # If "SSL" was defined to be an acronym: # # humanize('ssl_error') # => "SSL error" # - def humanize(lower_case_and_underscored_word, options = {}) + def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false) result = lower_case_and_underscored_word.to_s.dup inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } result.sub!(/\A_+/, "".freeze) - result.sub!(/_id\z/, "".freeze) + unless keep_id_suffix + result.sub!(/_id\z/, "".freeze) + end result.tr!("_".freeze, " ".freeze) result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" end - if options.fetch(:capitalize, true) + if capitalize result.sub!(/\A\w/) { |match| match.upcase } end @@ -154,14 +159,21 @@ module ActiveSupport # create a nicer looking title. +titleize+ is meant for creating pretty # output. It is not used in the Rails internals. # + # The trailing '_id','Id'.. can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true. + # By default, this parameter is false. + # # +titleize+ is also aliased as +titlecase+. # - # titleize('man from the boondocks') # => "Man From The Boondocks" - # titleize('x-men: the last stand') # => "X Men: The Last Stand" - # titleize('TheManWithoutAPast') # => "The Man Without A Past" - # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark" - def titleize(word) - humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize } + # titleize('man from the boondocks') # => "Man From The Boondocks" + # titleize('x-men: the last stand') # => "X Men: The Last Stand" + # titleize('TheManWithoutAPast') # => "The Man Without A Past" + # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark" + # titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id" + def titleize(word, keep_id_suffix: false) + humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`])[a-z]/) do |match| + match.capitalize + end end # Creates the name of a table like Rails does for models to table names. @@ -198,16 +210,16 @@ module ActiveSupport # Removes the module part from the expression in the string. # - # demodulize('ActiveRecord::CoreExtensions::String::Inflections') # => "Inflections" - # demodulize('Inflections') # => "Inflections" - # demodulize('::Inflections') # => "Inflections" - # demodulize('') # => "" + # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections" + # demodulize('Inflections') # => "Inflections" + # demodulize('::Inflections') # => "Inflections" + # demodulize('') # => "" # # See also #deconstantize. def demodulize(path) path = path.to_s if i = path.rindex("::") - path[(i+2)..-1] + path[(i + 2)..-1] else path end @@ -274,7 +286,7 @@ module ActiveSupport # Go down the ancestors to check if it is owned directly. The check # stops when we reach Object or the end of ancestors tree. - constant = constant.ancestors.inject do |const, ancestor| + constant = constant.ancestors.inject(constant) do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) const @@ -361,12 +373,12 @@ module ActiveSupport # # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" # const_regexp("::") # => "::" - def const_regexp(camel_cased_word) #:nodoc: + def const_regexp(camel_cased_word) parts = camel_cased_word.split("::".freeze) return Regexp.escape(camel_cased_word) if parts.blank? - last = parts.pop + last = parts.pop parts.reverse.inject(last) do |acc, part| part.empty? ? acc : "#{part}(::#{acc})?" @@ -375,12 +387,15 @@ module ActiveSupport # Applies inflection rules for +singularize+ and +pluralize+. # - # apply_inflections('post', inflections.plurals) # => "posts" - # apply_inflections('posts', inflections.singulars) # => "post" - def apply_inflections(word, rules) + # If passed an optional +locale+ parameter, the uncountables will be + # found for that locale. + # + # apply_inflections('post', inflections.plurals, :en) # => "posts" + # apply_inflections('posts', inflections.singulars, :en) # => "post" + def apply_inflections(word, rules, locale = :en) result = word.to_s.dup - if word.empty? || inflections.uncountables.uncountable?(result) + if word.empty? || inflections(locale).uncountables.uncountable?(result) result else rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 85fa83c803..de6dd2720b 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -57,6 +57,8 @@ module ActiveSupport # transliterate('Jürgen') # # => "Juergen" def transliterate(string, replacement = "?".freeze) + raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) + I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize( ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), replacement: replacement) @@ -78,11 +80,7 @@ module ActiveSupport # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" # parameterize("^trés|Jolie-- ", preserve_case: true) # => "tres-Jolie" # - def parameterize(string, sep = :unused, separator: "-", preserve_case: false) - unless sep == :unused - ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") - separator = sep - end + def parameterize(string, separator: "-", preserve_case: false) # Replace accented chars with their ASCII equivalents. parameterized_string = transliterate(string) diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index cee731417f..defaf3f395 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -68,7 +68,8 @@ module ActiveSupport :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString # Convert an object into a "JSON-ready" representation composed of - # primitives like Hash, Array, String, Numeric, and true/false/nil. + # primitives like Hash, Array, String, Numeric, + # and +true+/+false+/+nil+. # Recursively calls #as_json to the object to recursively build a # fully JSON-ready object. # @@ -84,7 +85,7 @@ module ActiveSupport when String EscapedString.new(value) when Numeric, NilClass, TrueClass, FalseClass - value + value.as_json when Hash Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] when Array diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 1064398d8c..23ab804eb1 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -17,7 +17,7 @@ module ActiveSupport # Returns a derived key suitable for use. The default key_size is chosen # to be compatible with the default settings of ActiveSupport::MessageVerifier. # i.e. OpenSSL::Digest::SHA1#block_length - def generate_key(salt, key_size=64) + def generate_key(salt, key_size = 64) OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) end end diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index b84c7253a0..720ed47331 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -15,16 +15,16 @@ module ActiveSupport # end # end # - # When the entirety of +activerecord/lib/active_record/base.rb+ has been + # When the entirety of +ActiveRecord::Base+ has been # evaluated then +run_load_hooks+ is invoked. The very last line of - # +activerecord/lib/active_record/base.rb+ is: + # +ActiveRecord::Base+ is: # # ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) module LazyLoadHooks def self.extended(base) # :nodoc: base.class_eval do - @load_hooks = Hash.new { |h,k| h[k] = [] } - @loaded = Hash.new { |h,k| h[k] = [] } + @load_hooks = Hash.new { |h, k| h[k] = [] } + @loaded = Hash.new { |h, k| h[k] = [] } end end diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e5812d75d0..e2c4f33565 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -85,7 +85,7 @@ module ActiveSupport logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" end - protected + private %w(info debug warn error fatal unknown).each do |level| class_eval <<-METHOD, __FILE__, __LINE__ + 1 @@ -99,7 +99,7 @@ module ActiveSupport # option is set to +true+, it also adds bold to the string. This is based # on the Highline implementation and will automatically append CLEAR to the # end of the returned String. - def color(text, color, bold=false) + def color(text, color, bold = false) # :doc: return text unless colorize_logging color = self.class.const_get(color.upcase) if color.is_a?(Symbol) bold = bold ? BOLD : "" diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index a7e30632f6..953ee77c2a 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -58,7 +58,7 @@ module ActiveSupport def initialize(level = DEBUG) @flush_count = 0 @level = level - @logged = Hash.new { |h,k| h[k] = [] } + @logged = Hash.new { |h, k| h[k] = [] } end def method_missing(level, message = nil) diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 3ba6461b57..ea09d7d2df 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -59,14 +59,14 @@ module ActiveSupport define_method(:silence) do |level = Logger::ERROR, &block| if logger.respond_to?(:silence) logger.silence(level) do - if respond_to?(:silence) + if defined?(super) super(level, &block) else block.call(self) end end else - if respond_to?(:silence) + if defined?(super) super(level, &block) else block.call(self) diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 7b33dc3481..726e1464ad 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -14,10 +14,10 @@ module ActiveSupport # where you don't want users to be able to determine the value of the payload. # # salt = SecureRandom.random_bytes(64) - # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." - # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> - # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." - # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" + # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, 32) # => "\x89\xE0\x156\xAC..." + # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> + # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." + # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor DEFAULT_CIPHER = "aes-256-cbc" @@ -50,6 +50,11 @@ module ActiveSupport # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key # derivation function. # + # First additional parameter is used as the signature key for +MessageVerifier+. + # This allows you to specify keys to encrypt and sign data. + # + # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret') + # # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. @@ -61,7 +66,7 @@ module ActiveSupport sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret - @cipher = options[:cipher] || "aes-256-cbc" + @cipher = options[:cipher] || DEFAULT_CIPHER @digest = options[:digest] || "SHA1" unless aead_mode? @verifier = resolve_verifier @serializer = options[:serializer] || Marshal @@ -110,7 +115,7 @@ module ActiveSupport # Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See # https://github.com/ruby/openssl/issues/63 - raise InvalidMessage if aead_mode? && auth_tag.bytes.length != 16 + raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16) cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 938e4ebb72..8c58466556 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -16,7 +16,8 @@ module ActiveSupport #:nodoc: # through the +mb_chars+ method. Methods which would normally return a # String object now return a Chars object so methods can be chained. # - # 'The Perfect String '.mb_chars.downcase.strip.normalize # => "the perfect string" + # 'The Perfect String '.mb_chars.downcase.strip.normalize + # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string"> # # Chars objects are perfectly interchangeable with String objects as long as # no explicit class checks are made. If certain methods do explicitly check @@ -87,7 +88,7 @@ module ActiveSupport #:nodoc: end # Works like <tt>String#slice!</tt>, but returns an instance of - # Chars, or nil if the string was not modified. The string will not be + # Chars, or +nil+ if the string was not modified. The string will not be # modified if the range given is out of bounds # # string = 'Welcome' @@ -134,7 +135,7 @@ module ActiveSupport #:nodoc: # Converts characters in the string to the opposite case. # - # 'El Cañón".mb_chars.swapcase.to_s # => "eL cAÑÓN" + # 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN" def swapcase chars Unicode.swapcase(@wrapped_string) end @@ -148,8 +149,8 @@ module ActiveSupport #:nodoc: # Capitalizes the first letter of every word, when possible. # - # "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró" - # "日本語".mb_chars.titleize # => "日本語" + # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró" + # "日本語".mb_chars.titleize.to_s # => "日本語" def titleize chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) }) end @@ -210,9 +211,9 @@ module ActiveSupport #:nodoc: end end - protected + private - def translate_offset(byte_offset) #:nodoc: + def translate_offset(byte_offset) return nil if byte_offset.nil? return 0 if @wrapped_string == "" @@ -224,7 +225,7 @@ module ActiveSupport #:nodoc: end end - def chars(string) #:nodoc: + def chars(string) self.class.new(string) end end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 2159abef14..8223e45e5a 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -9,7 +9,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = "8.0.0" + UNICODE_VERSION = "9.0.0" # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -30,36 +30,6 @@ module ActiveSupport HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT HANGUL_SCOUNT = 11172 HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT - HANGUL_JAMO_FIRST = 0x1100 - HANGUL_JAMO_LAST = 0x11FF - - # All the unicode whitespace - WHITESPACE = [ - (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D> - 0x0020, # White_Space # Zs SPACE - 0x0085, # White_Space # Cc <control-0085> - 0x00A0, # White_Space # Zs NO-BREAK SPACE - 0x1680, # White_Space # Zs OGHAM SPACE MARK - (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE - 0x2028, # White_Space # Zl LINE SEPARATOR - 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR - 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE - 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE - 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE - ].flatten.freeze - - # BOM (byte order mark) can also be seen as whitespace, it's a - # non-rendering character used to distinguish between little and big - # endian. This is not an issue in utf-8, so it must be ignored. - LEADERS_AND_TRAILERS = WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM - - # Returns a regular expression pattern that matches the passed Unicode - # codepoints. - def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: - array_of_codepoints.collect { |e| [e].pack "U*".freeze }.join("|".freeze) - end - TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u - LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u # Detect whether the codepoint is in a certain character class. Returns # +true+ when it's in the specified character class and +false+ otherwise. @@ -82,35 +52,35 @@ module ActiveSupport pos = 0 marker = 0 eoc = codepoints.length - while(pos < eoc) + while (pos < eoc) pos += 1 - previous = codepoints[pos-1] + previous = codepoints[pos - 1] current = codepoints[pos] + # See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules should_break = + if pos == eoc + true # GB3. CR X LF - if previous == database.boundary[:cr] && current == database.boundary[:lf] + elsif previous == database.boundary[:cr] && current == database.boundary[:lf] false # GB4. (Control|CR|LF) ÷ - elsif previous && in_char_class?(previous, [:control,:cr,:lf]) + elsif previous && in_char_class?(previous, [:control, :cr, :lf]) true # GB5. ÷ (Control|CR|LF) - elsif in_char_class?(current, [:control,:cr,:lf]) + elsif in_char_class?(current, [:control, :cr, :lf]) true # GB6. L X (L|V|LV|LVT) - elsif database.boundary[:l] === previous && in_char_class?(current, [:l,:v,:lv,:lvt]) + elsif database.boundary[:l] === previous && in_char_class?(current, [:l, :v, :lv, :lvt]) false # GB7. (LV|V) X (V|T) - elsif in_char_class?(previous, [:lv,:v]) && in_char_class?(current, [:v,:t]) + elsif in_char_class?(previous, [:lv, :v]) && in_char_class?(current, [:v, :t]) false # GB8. (LVT|T) X (T) - elsif in_char_class?(previous, [:lvt,:t]) && database.boundary[:t] === current - false - # GB8a. Regional_Indicator X Regional_Indicator - elsif database.boundary[:regional_indicator] === previous && database.boundary[:regional_indicator] === current + elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current false - # GB9. X Extend - elsif database.boundary[:extend] === current + # GB9. X (Extend | ZWJ) + elsif in_char_class?(current, [:extend, :zwj]) false # GB9a. X SpacingMark elsif database.boundary[:spacingmark] === current @@ -118,13 +88,23 @@ module ActiveSupport # GB9b. Prepend X elsif database.boundary[:prepend] === previous false - # GB10. Any ÷ Any + # GB10. (E_Base | EBG) Extend* X E_Modifier + elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current + false + # GB11. ZWJ X (Glue_After_Zwj | EBG) + elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz]) + false + # GB12. ^ (RI RI)* RI X RI + # GB13. [^RI] (RI RI)* RI X RI + elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even? + false + # GB999. Any ÷ Any else true end if should_break - unpacked << codepoints[marker..pos-1] + unpacked << codepoints[marker..pos - 1] marker = pos end end @@ -140,12 +120,12 @@ module ActiveSupport # Re-order codepoints so the string becomes canonical. def reorder_characters(codepoints) - length = codepoints.length- 1 + length = codepoints.length - 1 pos = 0 while pos < length do - cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos+1]] + cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos + 1]] if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0) - codepoints[pos..pos+1] = cp2.code, cp1.code + codepoints[pos..pos + 1] = cp2.code, cp1.code pos += (pos > 0 ? -1 : 1) else pos += 1 @@ -187,9 +167,9 @@ module ActiveSupport lindex = starter_char - HANGUL_LBASE # -- Hangul if 0 <= lindex && lindex < HANGUL_LCOUNT - vindex = codepoints[starter_pos+1] - HANGUL_VBASE rescue vindex = -1 + vindex = codepoints[starter_pos + 1] - HANGUL_VBASE rescue vindex = -1 if 0 <= vindex && vindex < HANGUL_VCOUNT - tindex = codepoints[starter_pos+2] - HANGUL_TBASE rescue tindex = -1 + tindex = codepoints[starter_pos + 2] - HANGUL_TBASE rescue tindex = -1 if 0 <= tindex && tindex < HANGUL_TCOUNT j = starter_pos + 2 eoa -= 2 @@ -281,7 +261,7 @@ module ActiveSupport # * <tt>form</tt> - The form you want to normalize in. Should be one of # the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. - def normalize(string, form=nil) + def normalize(string, form = nil) form ||= @default_normalization_form # See http://www.unicode.org/reports/tr15, Table 1 codepoints = string.codepoints.to_a @@ -377,7 +357,7 @@ module ActiveSupport # Returns the directory in which the data files are stored. def self.dirname - File.dirname(__FILE__) + "/../values/" + File.expand_path("../values", __dir__) end # Returns the filename for the data file for this version. @@ -388,7 +368,7 @@ module ActiveSupport private - def apply_mapping(string, mapping) #:nodoc: + def apply_mapping(string, mapping) database.codepoints string.each_codepoint.map do |codepoint| cp = database.codepoints[codepoint] diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index bae5f067ae..37dfdc0422 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -13,7 +13,7 @@ module ActiveSupport # To instrument an event you just need to do: # # ActiveSupport::Notifications.instrument('render', extra: :information) do - # render text: 'Foo' + # render plain: 'Foo' # end # # That first executes the block and then notifies all subscribers once done. @@ -48,7 +48,7 @@ module ActiveSupport # The block is saved and will be called whenever someone instruments "render": # # ActiveSupport::Notifications.instrument('render', extra: :information) do - # render text: 'Foo' + # render plain: 'Foo' # end # # event = events.first @@ -64,6 +64,8 @@ module ActiveSupport # If an exception happens during that particular instrumentation the payload will # have a key <tt>:exception</tt> with an array of two elements as value: a string with # the name of the exception class, and the exception message. + # The <tt>:exception_object</tt> key of the payload will have the exception + # itself as the value. # # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> # is able to take the arguments as they come and provide an object-oriented diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 23262d5398..e11e2e0689 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -14,7 +14,7 @@ module ActiveSupport # Instrument the given block by measuring the time taken to execute it # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. - def instrument(name, payload={}) + def instrument(name, payload = {}) # some of the listeners might have state listeners_state = start name, payload begin diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 7a49bbb960..9cb2821cb6 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -4,6 +4,7 @@ module ActiveSupport eager_autoload do autoload :NumberConverter + autoload :RoundingHelper autoload :NumberToRoundedConverter autoload :NumberToDelimitedConverter autoload :NumberToHumanConverter @@ -45,7 +46,7 @@ module ActiveSupport # # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true) # # => "(755) 6123-4567" - # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)) + # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/) # # => "133-1234-5678" def number_to_phone(number, options = {}) NumberToPhoneConverter.convert(number, options) @@ -78,7 +79,7 @@ module ActiveSupport # (defaults to "%u%n"). Fields are <tt>%u</tt> for the # currency, and <tt>%n</tt> for the number. # * <tt>:negative_format</tt> - Sets the format for negative - # numbers (defaults to prepending an hyphen to the formatted + # numbers (defaults to prepending a hyphen to the formatted # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. @@ -109,7 +110,7 @@ module ActiveSupport # * <tt>:locale</tt> - Sets the locale to be used for formatting # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number - # (defaults to 3). Keeps the number's precision if nil. + # (defaults to 3). Keeps the number's precision if +nil+. # * <tt>:significant</tt> - If +true+, precision will be the number # of significant_digits. If +false+, the number of fractional # digits (defaults to +false+). @@ -183,7 +184,7 @@ module ActiveSupport # * <tt>:locale</tt> - Sets the locale to be used for formatting # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number - # (defaults to 3). Keeps the number's precision if nil. + # (defaults to 3). Keeps the number's precision if +nil+. # * <tt>:significant</tt> - If +true+, precision will be the number # of significant_digits. If +false+, the number of fractional # digits (defaults to +false+). diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb index c485a6af63..ce363287cf 100644 --- a/activesupport/lib/active_support/number_helper/number_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_converter.rb @@ -139,17 +139,17 @@ module ActiveSupport @options ||= format_options.merge(opts) end - def format_options #:nodoc: + def format_options default_format_options.merge!(i18n_format_options) end - def default_format_options #:nodoc: + def default_format_options options = DEFAULTS[:format].dup options.merge!(DEFAULTS[namespace][:format]) if namespace options end - def i18n_format_options #:nodoc: + def i18n_format_options locale = opts[:locale] options = I18n.translate(:'number.format', locale: locale, default: {}).dup @@ -160,7 +160,7 @@ module ActiveSupport options end - def translate_number_value_with_default(key, i18n_options = {}) #:nodoc: + def translate_number_value_with_default(key, i18n_options = {}) I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options)) end @@ -172,7 +172,7 @@ module ActiveSupport key.split(".").reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] } end - def valid_float? #:nodoc: + def valid_float? Float(number) rescue ArgumentError, TypeError false diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 695ee1dad3..040343b5dd 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -9,6 +9,7 @@ module ActiveSupport self.validate_float = true def convert # :nodoc: + @number = RoundingHelper.new(options).round(number) @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files @@ -18,12 +19,9 @@ module ActiveSupport units = opts[:units] exponent = calculate_exponent(units) - @number = number / (10 ** exponent) + @number = number / (10**exponent) - until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options) - @number = number / 1000.0 - exponent += 3 - end + rounded_number = NumberToRoundedConverter.convert(number, options) unit = determine_unit(units, exponent) format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 78cb33aa62..f263dbe9f8 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -7,10 +7,6 @@ module ActiveSupport self.validate_float = true def convert - if opts.key?(:prefix) - ActiveSupport::Deprecation.warn("The :prefix option of `number_to_human_size` is deprecated and will be removed in Rails 5.1 with no replacement.") - end - @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files @@ -21,7 +17,7 @@ module ActiveSupport if smaller_than_base? number_to_format = number.to_i.to_s else - human_size = number / (base ** exponent) + human_size = number / (base**exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end conversion_format.gsub("%n".freeze, number_to_format).gsub("%u".freeze, unit) @@ -54,7 +50,7 @@ module ActiveSupport end def base - opts[:prefix] == :si ? 1000 : 1024 + 1024 end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index c2612f9a9b..1de9f50f34 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -2,7 +2,7 @@ module ActiveSupport module NumberHelper class NumberToPhoneConverter < NumberConverter #:nodoc: def convert - str = country_code(opts[:country_code]) + str = country_code(opts[:country_code]) str << convert_to_phone_number(number.to_s.strip) str << phone_ext(opts[:extension]) end diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index cfcb0045fb..c32d85a45f 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -5,26 +5,14 @@ module ActiveSupport self.validate_float = true def convert - precision = options.delete :precision + helper = RoundingHelper.new(options) + rounded_number = helper.round(number) - if precision - case number - when Float, String - @number = BigDecimal(number.to_s) - when Rational - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = number.to_d - end - - if options.delete(:significant) && precision > 0 - digits, rounded_number = digits_and_rounded_number(precision) + if precision = options[:precision] + if options[:significant] && precision > 0 + digits = helper.digit_count(rounded_number) precision -= digits precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = number.round(precision) - rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite? - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end formatted_string = @@ -38,7 +26,7 @@ module ActiveSupport "%00.#{precision}f" % rounded_number end else - formatted_string = number + formatted_string = rounded_number end delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) @@ -52,7 +40,7 @@ module ActiveSupport [1, 0] else digits = digit_count(number) - multiplier = 10 ** (digits - precision) + multiplier = 10**(digits - precision) rounded_number = calculate_rounded_number(multiplier) digits = digit_count(rounded_number) # After rounding, the number of digits may have changed [digits, rounded_number] @@ -79,14 +67,6 @@ module ActiveSupport number end end - - def absolute_number(number) - number.respond_to?(:abs) ? number.abs : number.to_d.abs - end - - def zero? - number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? - end end end end diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb new file mode 100644 index 0000000000..d9644df17d --- /dev/null +++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb @@ -0,0 +1,64 @@ +module ActiveSupport + module NumberHelper + class RoundingHelper # :nodoc: + attr_reader :options + + def initialize(options) + @options = options + end + + def round(number) + return number unless precision + number = convert_to_decimal(number) + if significant && precision > 0 + round_significant(number) + else + round_without_significant(number) + end + end + + def digit_count(number) + return 1 if number.zero? + (Math.log10(absolute_number(number)) + 1).floor + end + + private + def round_without_significant(number) + number = number.round(precision) + number = number.to_i if precision == 0 && number.finite? + number = number.abs if number.zero? # prevent showing negative zeros + number + end + + def round_significant(number) + return 0 if number.zero? + digits = digit_count(number) + multiplier = 10**(digits - precision) + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier + end + + def convert_to_decimal(number) + case number + when Float, String + number = BigDecimal(number.to_s) + when Rational + number = BigDecimal(number, digit_count(number.to_i) + precision) + else + number = number.to_d + end + end + + def precision + options[:precision] + end + + def significant + options[:significant] + end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + end + end +end diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 793bc9e22d..3aa0a14f04 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -25,7 +25,7 @@ module ActiveSupport end def encode_with(coder) - coder.represent_seq "!omap", map { |k,v| { k => v } } + coder.represent_seq "!omap", map { |k, v| { k => v } } end def select(*args, &block) diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 14ed9049be..04d6dfaf9c 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -68,9 +68,9 @@ module ActiveSupport def initialize(parent = nil) if parent.kind_of?(OrderedOptions) # use the faster _get when dealing with OrderedOptions - super() { |h,k| parent._get(k) } + super() { |h, k| parent._get(k) } elsif parent - super() { |h,k| parent[k] } + super() { |h, k| parent[k] } else super() end diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index 9e6d8d4fd8..02431704d3 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -45,8 +45,8 @@ module ActiveSupport Thread.current[@per_thread_registry_key] ||= new end - protected - def method_missing(name, *args, &block) # :nodoc: + private + def method_missing(name, *args, &block) # Caches the method definition as a singleton method of the receiver. # # By letting #delegate handle it, we avoid an enclosure that'll capture args. diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb index 57380061f7..f6b018f0d3 100644 --- a/activesupport/lib/active_support/rails.rb +++ b/activesupport/lib/active_support/rails.rb @@ -25,3 +25,9 @@ require "active_support/core_ext/module/delegation" # Defines ActiveSupport::Deprecation. require "active_support/deprecation" + +# Defines Regexp#match?. +# +# This should be removed when Rails needs Ruby 2.4 or later, and the require +# added where other Regexp extensions are being used (easy to grep). +require "active_support/core_ext/regexp" diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index b875875afe..1b4ecf4d72 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -7,6 +7,12 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport + initializer "active_support.reset_all_current_attributes_instances" do |app| + app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all } + app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } + app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } + end + initializer "active_support.deprecation_behavior" do |app| if deprecation = app.config.active_support.deprecation ActiveSupport::Deprecation.behavior = deprecation diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index 121c621751..9558146201 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -69,11 +69,8 @@ module ActiveSupport end end - class_attribute :executor - class_attribute :check - - self.executor = Executor - self.check = lambda { false } + class_attribute :executor, default: Executor + class_attribute :check, default: lambda { false } def self.check! # :nodoc: @should_reload ||= check.call diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index dc3f27a16d..826832ba7d 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -8,8 +8,7 @@ module ActiveSupport extend Concern included do - class_attribute :rescue_handlers - self.rescue_handlers = [] + class_attribute :rescue_handlers, default: [] end module ClassMethods @@ -36,7 +35,7 @@ module ActiveSupport # render xml: exception, status: 500 # end # - # protected + # private # def deny_access # ... # end @@ -74,7 +73,7 @@ module ActiveSupport # # If no handler matches the exception, check for a handler matching the # (optional) exception.cause. If no handler matches the exception or its - # cause, this returns nil so you can deal with unhandled exceptions. + # cause, this returns +nil+, so you can deal with unhandled exceptions. # Be sure to re-raise unhandled exceptions if this is what you expect. # # begin @@ -83,11 +82,19 @@ module ActiveSupport # rescue_with_handler(exception) || raise # end # - # Returns the exception if it was handled and nil if it was not. - def rescue_with_handler(exception, object: self) + # Returns the exception if it was handled and +nil+ if it was not. + def rescue_with_handler(exception, object: self, visited_exceptions: []) + visited_exceptions << exception + if handler = handler_for_rescue(exception, object: object) handler.call exception exception + elsif exception + if visited_exceptions.include?(exception.cause) + nil + else + rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions) + end end end @@ -121,7 +128,7 @@ module ActiveSupport end end - handler || find_rescue_handler(exception.cause) + handler end end diff --git a/activesupport/lib/active_support/string_inquirer.rb b/activesupport/lib/active_support/string_inquirer.rb index 09e1cbb28d..90eac89c9e 100644 --- a/activesupport/lib/active_support/string_inquirer.rb +++ b/activesupport/lib/active_support/string_inquirer.rb @@ -18,7 +18,7 @@ module ActiveSupport private def respond_to_missing?(method_name, include_private = false) - method_name[-1] == "?" + (method_name[-1] == "?") || super end def method_missing(method_name, *arguments) diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 0f09f1eb63..2924139755 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -1,4 +1,5 @@ require "active_support/per_thread_registry" +require "active_support/notifications" module ActiveSupport # ActiveSupport::Subscriber is an object set to consume @@ -24,7 +25,7 @@ module ActiveSupport class Subscriber class << self # Attach the subscriber to a namespace. - def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications) + def attach_to(namespace, subscriber = new, notifier = ActiveSupport::Notifications) @namespace = namespace @subscriber = subscriber @notifier = notifier @@ -51,11 +52,15 @@ module ActiveSupport @@subscribers ||= [] end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :subscriber, :notifier, :namespace - def add_event_subscriber(event) + private + + def add_event_subscriber(event) # :doc: return if %w{ start finish }.include?(event.to_s) pattern = "#{event}.#{namespace}" diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index 6836378943..ad134c49b6 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -48,13 +48,12 @@ module ActiveSupport Thread.current[thread_key] ||= [] end - private - def tags_text - tags = current_tags - if tags.any? - tags.collect { |tag| "[#{tag}] " }.join - end + def tags_text + tags = current_tags + if tags.any? + tags.collect { |tag| "[#{tag}] " }.join end + end end def self.new(logger) diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 1c599b8851..3de4ccc1da 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -65,5 +65,7 @@ module ActiveSupport alias :assert_not_predicate :refute_predicate alias :assert_not_respond_to :refute_respond_to alias :assert_not_same :refute_same + + ActiveSupport.run_load_hooks(:active_support_test_case, self) end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 28cf2953bf..28e1df8870 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -167,7 +167,7 @@ module ActiveSupport retval end - # Assertion that the result of evaluating an expression is changed before + # Assertion that the result of evaluating an expression is not changed before # and after invoking the passed in block. # # assert_no_changes 'Status.all_good?' do diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb index 3108e3e549..a18788f38e 100644 --- a/activesupport/lib/active_support/testing/autorun.rb +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -2,8 +2,8 @@ gem "minitest" require "minitest" -if Minitest.respond_to?(:run_via) && !Minitest.run_via[:rails] - Minitest.run_via[:ruby] = true +if Minitest.respond_to?(:run_via) && !Minitest.run_via.set? + Minitest.run_via = :ruby end Minitest.autorun diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index 0bf3643a56..53ab3ebf78 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -9,7 +9,7 @@ module ActiveSupport # ... # end def test(name, &block) - test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym + test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym defined = method_defined? test_name raise "#{test_name} is already defined in #{self}" if defined if block_given? diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index d30b34ecd6..54c3263efa 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -43,7 +43,7 @@ module ActiveSupport end } end - result = Marshal.dump(self.dup) + result = Marshal.dump(dup) end write.puts [result].pack("m") @@ -78,13 +78,15 @@ module ActiveSupport "ISOLATION_OUTPUT" => tmpfile.path } - load_paths = $-I.map { |p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - orig_args = ORIG_ARGV.join(" ") - test_opts = "-n#{self.class.name}##{self.name}" - command = "#{Gem.ruby} #{load_paths} #{$0} '#{orig_args}' #{test_opts}" + test_opts = "-n#{self.class.name}##{name}" - # IO.popen lets us pass env in a cross-platform way - child = IO.popen(env, command) + load_path_args = [] + $-I.each do |p| + load_path_args << "-I" + load_path_args << File.expand_path(p) + end + + child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts]) begin Process.wait(child.pid) diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index e2f008b4b7..3d9ff99729 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/string/strip" # for strip_heredoc +require "active_support/core_ext/time/calculations" require "concurrent/map" module ActiveSupport @@ -10,7 +11,7 @@ module ActiveSupport @stubs = Concurrent::Map.new { |h, k| h[k] = {} } end - def stub_object(object, method_name, return_value) + def stub_object(object, method_name, &block) if stub = stubbing(object, method_name) unstub_object(stub) end @@ -20,7 +21,7 @@ module ActiveSupport @stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name) object.singleton_class.send :alias_method, new_name, method_name - object.define_singleton_method(method_name) { return_value } + object.define_singleton_method(method_name, &block) end def unstub_all! @@ -134,9 +135,9 @@ module ActiveSupport now = date_or_time.to_time.change(usec: 0) end - simple_stubs.stub_object(Time, :now, now) - simple_stubs.stub_object(Date, :today, now.to_date) - simple_stubs.stub_object(DateTime, :now, now.to_datetime) + simple_stubs.stub_object(Time, :now) { at(now.to_i) } + simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) } + simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) } if block_given? begin diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index c35588fbae..ecb9fb6785 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -80,7 +80,7 @@ module ActiveSupport # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone. def localtime(utc_offset = nil) - @localtime ||= utc.getlocal(utc_offset) + utc.getlocal(utc_offset) end alias_method :getlocal, :localtime @@ -148,6 +148,7 @@ module ActiveSupport "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}" end alias_method :iso8601, :xmlschema + alias_method :rfc3339, :xmlschema # Coerces time to a string for JSON encoding. The default format is ISO 8601. # You can get %Y/%m/%d %H:%M:%S +offset style by setting @@ -329,6 +330,42 @@ module ActiveSupport since(-other) end + # Returns a new +ActiveSupport::TimeWithZone+ where one or more of the elements have + # been changed according to the +options+ parameter. The time options (<tt>:hour</tt>, + # <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, + # so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the + # hour and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, + # <tt>:nsec</tt>, <tt>:offset</tt>, <tt>:zone</tt>. Pass either <tt>:usec</tt> + # or <tt>:nsec</tt>, not both. Similarly, pass either <tt>:zone</tt> or + # <tt>:offset</tt>, not both. + # + # t = Time.zone.now # => Fri, 14 Apr 2017 11:45:15 EST -05:00 + # t.change(year: 2020) # => Tue, 14 Apr 2020 11:45:15 EST -05:00 + # t.change(hour: 12) # => Fri, 14 Apr 2017 12:00:00 EST -05:00 + # t.change(min: 30) # => Fri, 14 Apr 2017 11:30:00 EST -05:00 + # t.change(offset: "-10:00") # => Fri, 14 Apr 2017 11:45:15 HST -10:00 + # t.change(zone: "Hawaii") # => Fri, 14 Apr 2017 11:45:15 HST -10:00 + def change(options) + if options[:zone] && options[:offset] + raise ArgumentError, "Can't change both :offset and :zone at the same time: #{options.inspect}" + end + + new_time = time.change(options) + + if options[:zone] + new_zone = ::Time.find_zone(options[:zone]) + elsif options[:offset] + new_zone = ::Time.find_zone(new_time.utc_offset) + end + + new_zone ||= time_zone + periods = new_zone.periods_for_local(new_time) + + self.class.new(nil, new_zone, new_time, periods.include?(period) ? period : nil) + end + # Uses Date to provide precise Time calculations for years, months, and days # according to the proleptic Gregorian calendar. The result is returned as a # new TimeWithZone object. @@ -410,6 +447,17 @@ module ActiveSupport @to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400)) end + # Returns an instance of +Time+, either with the same UTC offset + # as +self+ or in the local system timezone depending on the setting + # of +ActiveSupport.to_time_preserves_timezone+. + def to_time + if preserve_timezone + @to_time_with_instance_offset ||= getlocal(utc_offset) + else + @to_time_with_system_offset ||= getlocal + end + end + # So that +self+ <tt>acts_like?(:time)</tt>. def acts_like_time? true @@ -427,7 +475,8 @@ module ActiveSupport end def freeze - period; utc; time # preload instance variables before freezing + # preload instance variables before freezing + period; utc; time; to_datetime; to_time super end @@ -477,6 +526,8 @@ module ActiveSupport end def transfer_time_values_to_utc_constructor(time) + # avoid creating another Time object if possible + return time if time.instance_of?(::Time) && time.utc? ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec) end diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index cb97a0e135..96a541a4ef 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -6,7 +6,7 @@ module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. # It allows us to do the following: # - # * Limit the set of zones provided by TZInfo to a meaningful subset of 146 + # * Limit the set of zones provided by TZInfo to a meaningful subset of 134 # zones. # * Retrieve and display zones with a friendlier name # (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). @@ -59,6 +59,7 @@ module ActiveSupport "Buenos Aires" => "America/Argentina/Buenos_Aires", "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", + "Puerto Rico" => "America/Puerto_Rico", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", "Azores" => "Atlantic/Azores", @@ -250,14 +251,21 @@ module ActiveSupport # for time zones in the country specified by its ISO 3166-1 Alpha2 code. def country_zones(country_code) code = country_code.to_s.upcase - @country_zones[code] ||= - TZInfo::Country.get(code).zone_identifiers.map do |tz_id| - name = MAPPING.key(tz_id) - name && self[name] - end.compact.sort! + @country_zones[code] ||= load_country_zones(code) end private + def load_country_zones(code) + country = TZInfo::Country.get(code) + country.zone_identifiers.map do |tz_id| + if MAPPING.value?(tz_id) + self[MAPPING.key(tz_id)] + else + create(tz_id, nil, TZInfo::Timezone.new(tz_id)) + end + end.sort! + end + def zones_map @zones_map ||= begin MAPPING.each_key { |place| self[place] } # load all the zones @@ -295,7 +303,7 @@ module ActiveSupport # zone = ActiveSupport::TimeZone['Central Time (US & Canada)'] # zone.formatted_offset # => "-06:00" # zone.formatted_offset(false) # => "-0600" - def formatted_offset(colon=true, alternate_utc_string = nil) + def formatted_offset(colon = true, alternate_utc_string = nil) utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon) end @@ -340,6 +348,41 @@ module ActiveSupport end # Method for creating new ActiveSupport::TimeWithZone instance in time zone + # of +self+ from an ISO 8601 string. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.iso8601('1999-12-31T14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # + # If the time components are missing then they will be set to zero. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.iso8601('1999-12-31') # => Fri, 31 Dec 1999 00:00:00 HST -10:00 + # + # If the string is invalid then an +ArgumentError+ will be raised unlike +parse+ + # which returns +nil+ when given an invalid date string. + def iso8601(str) + parts = Date._iso8601(str) + + raise ArgumentError, "invalid date" if parts.empty? + + time = Time.new( + parts.fetch(:year), + parts.fetch(:mon), + parts.fetch(:mday), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, 0) + ) + + if parts[:offset] + TimeWithZone.new(time.utc, self) + else + TimeWithZone.new(nil, self, time) + end + end + + # Method for creating new ActiveSupport::TimeWithZone instance in time zone # of +self+ from parsed string. # # Time.zone = 'Hawaii' # => "Hawaii" @@ -355,10 +398,40 @@ module ActiveSupport # components are supplied, then the day of the month defaults to 1: # # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 - def parse(str, now=now()) + def parse(str, now = now()) parts_to_time(Date._parse(str, false), now) end + # Method for creating new ActiveSupport::TimeWithZone instance in time zone + # of +self+ from an RFC 3339 string. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.rfc3339('2000-01-01T00:00:00Z') # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # + # If the time or zone components are missing then an +ArgumentError+ will + # be raised. This is much stricter than either +parse+ or +iso8601+ which + # allow for missing components. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.rfc3339('1999-12-31') # => ArgumentError: invalid date + def rfc3339(str) + parts = Date._rfc3339(str) + + raise ArgumentError, "invalid date" if parts.empty? + + time = Time.new( + parts.fetch(:year), + parts.fetch(:mon), + parts.fetch(:mday), + parts.fetch(:hour), + parts.fetch(:min), + parts.fetch(:sec) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset) + ) + + TimeWithZone.new(time.utc, self) + end + # Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone. # # Assumes that +str+ is a time in the time zone +self+, @@ -379,7 +452,7 @@ module ActiveSupport # components are supplied, then the day of the month defaults to 1: # # Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 - def strptime(str, format, now=now()) + def strptime(str, format, now = now()) parts_to_time(DateTime._strptime(str, format), now) end @@ -416,7 +489,7 @@ module ActiveSupport # Adjust the given time to the simultaneous time in UTC. Returns a # Time.utc() instance. - def local_to_utc(time, dst=true) + def local_to_utc(time, dst = true) tzinfo.local_to_utc(time, dst) end @@ -428,7 +501,7 @@ module ActiveSupport # Available so that TimeZone instances respond like TZInfo::Timezone # instances. - def period_for_local(time, dst=true) + def period_for_local(time, dst = true) tzinfo.period_for_local(time, dst) end @@ -441,7 +514,7 @@ module ActiveSupport end def encode_with(coder) #:nodoc: - coder.tag ="!ruby/object:#{self.class}" + coder.tag = "!ruby/object:#{self.class}" coder.map = { "name" => tzinfo.name } end @@ -450,17 +523,21 @@ module ActiveSupport raise ArgumentError, "invalid date" if parts.nil? return if parts.empty? - time = Time.new( - parts.fetch(:year, now.year), - parts.fetch(:mon, now.month), - parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), - parts.fetch(:hour, 0), - parts.fetch(:min, 0), - parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), - parts.fetch(:offset, 0) - ) - - if parts[:offset] + if parts[:seconds] + time = Time.at(parts[:seconds]) + else + time = Time.new( + parts.fetch(:year, now.year), + parts.fetch(:mon, now.month), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, 0) + ) + end + + if parts[:offset] || parts[:seconds] TimeWithZone.new(time.utc, self) else TimeWithZone.new(nil, self, time) diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex dd2c178fb6..f7d9c48bbe 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 46b91806f6..782fb41288 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -48,8 +48,8 @@ module ActiveSupport } # No need to map these on Ruby 2.4+ - TYPE_NAMES["Fixnum"] = "integer" unless Fixnum == Integer - TYPE_NAMES["Bignum"] = "integer" unless Bignum == Integer + TYPE_NAMES["Fixnum"] = "integer" unless 0.class == Integer + TYPE_NAMES["Bignum"] = "integer" unless 0.class == Integer end FORMATTING = { @@ -68,7 +68,17 @@ module ActiveSupport "datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc }, "integer" => Proc.new { |integer| integer.to_i }, "float" => Proc.new { |float| float.to_f }, - "decimal" => Proc.new { |number| BigDecimal(number) }, + "decimal" => Proc.new do |number| + if String === number + begin + BigDecimal(number) + rescue ArgumentError + BigDecimal("0") + end + else + BigDecimal(number) + end + end, "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) }, "string" => Proc.new { |string| string.to_s }, "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, @@ -149,16 +159,16 @@ module ActiveSupport key end - protected + private def _dasherize(key) # $2 must be a non-greedy regex for this to work - left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1,3] + left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1, 3] "#{left}#{middle.tr('_ ', '--')}#{right}" end # TODO: Add support for other encodings - def _parse_binary(bin, entity) #:nodoc: + def _parse_binary(bin, entity) case entity["encoding"] when "base64" ::Base64.decode64(bin) @@ -175,8 +185,6 @@ module ActiveSupport f end - private - def current_thread_backend Thread.current[:xml_mini_backend] end diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 10498c9be0..a7939b3185 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -141,7 +141,7 @@ module ActiveSupport attributes = element.attributes (0...attributes.length).each do |i| attribute_hash[CONTENT_KEY] ||= "" - attribute_hash[attributes.item(i).name] = attributes.item(i).value + attribute_hash[attributes.item(i).name] = attributes.item(i).value end attribute_hash end diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 8a4aa03963..d849cdfa6b 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -14,11 +14,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) LibXML::XML::Parser.io(data).parse.to_hash end end @@ -40,7 +38,7 @@ module LibXML #:nodoc: # # hash:: # Hash to merge the converted element into. - def to_hash(hash={}) + def to_hash(hash = {}) node_hash = {} # Insert node hash into parent hash correctly. @@ -74,5 +72,7 @@ module LibXML #:nodoc: end end +# :enddoc: + LibXML::XML::Document.include(LibXML::Conversions::Document) LibXML::XML::Node.include(LibXML::Conversions::Node) diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index 4edb0bd2b1..f3d485da2f 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -65,15 +65,12 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) - LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) parser = LibXML::XML::SaxParser.io(data) - document = self.document_class.new + document = document_class.new parser.callbacks = document parser.parse diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index b92fa7ea7c..63466c08b2 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -19,11 +19,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) doc = Nokogiri::XML(data) raise doc.errors.first if doc.errors.length > 0 doc.to_hash @@ -44,7 +42,7 @@ module ActiveSupport # # hash:: # Hash to merge the converted element into. - def to_hash(hash={}) + def to_hash(hash = {}) node_hash = {} # Insert node hash into parent hash correctly. diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index b8b85146c5..54e15e6a5f 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -71,12 +71,10 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) - document = self.document_class.new + document = document_class.new parser = Nokogiri::XML::SAX::Parser.new(document) parser.parse(data) document.hash diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 091a15294c..03fa910fa5 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -113,7 +113,7 @@ module ActiveSupport # XML element to extract attributes from. def get_attributes(element) attributes = {} - element.attributes.each { |n,v| attributes[n] = v } + element.attributes.each { |n, v| attributes[n] = v } attributes end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 4e564591b4..c4f34c0abf 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -3,8 +3,8 @@ ORIG_ARGV = ARGV.dup require "active_support/core_ext/kernel/reporting" silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end require "active_support/testing/autorun" @@ -24,16 +24,16 @@ ActiveSupport.to_time_preserves_timezone = ENV["PRESERVE_TIMEZONES"] == "1" # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end - -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb index 4d3f5b001c..5b2bc82905 100644 --- a/activesupport/test/array_inquirer_test.rb +++ b/activesupport/test/array_inquirer_test.rb @@ -38,4 +38,24 @@ class ArrayInquirerTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::ArrayInquirer, result assert_equal @array_inquirer, result end + + def test_respond_to_fallback_to_array_respond_to + Array.class_eval do + def respond_to_missing?(name, include_private = false) + (name == :foo) || super + end + end + arr = ActiveSupport::ArrayInquirer.new([:x]) + + assert_respond_to arr, :can_you_hear_me? + assert_respond_to arr, :foo + assert_not_respond_to arr, :nope + ensure + Array.class_eval do + undef_method :respond_to_missing? + def respond_to_missing?(name, include_private = false) + super + end + end + end end diff --git a/activesupport/test/autoloading_fixtures/prepend.rb b/activesupport/test/autoloading_fixtures/prepend.rb new file mode 100644 index 0000000000..3134d1df2b --- /dev/null +++ b/activesupport/test/autoloading_fixtures/prepend.rb @@ -0,0 +1,8 @@ +class SubClassConflict +end + +class Prepend + module PrependedModule + end + prepend PrependedModule +end diff --git a/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb b/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb new file mode 100644 index 0000000000..090dda3043 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb @@ -0,0 +1,2 @@ +class Prepend::SubClassConflict +end diff --git a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb index 829ee917d2..e477ab21d0 100644 --- a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb +++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb @@ -1,4 +1,4 @@ RaisesArbitraryException = 1 _ = A::B # Autoloading recursion, also expected to be watched and discarded. -raise Exception, "arbitray exception message" +raise Exception, "arbitrary exception message" diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index 4b74f1313e..184d0ebddd 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -69,6 +69,20 @@ module ActiveSupport assert_equal ::Logger::FATAL, log2.local_level end + test "#silence does not break custom loggers" do + new_logger = FakeLogger.new + custom_logger = CustomLogger.new + custom_logger.extend(Logger.broadcast(new_logger)) + + custom_logger.silence do + custom_logger.error "from error" + custom_logger.unknown "from unknown" + end + + assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], custom_logger.adds + assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], new_logger.adds + end + test "#silence silences all loggers below the default level of ERROR" do logger.silence do logger.debug "test" @@ -98,9 +112,7 @@ module ActiveSupport assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds end - class FakeLogger - include LoggerSilence - + class CustomLogger attr_reader :adds, :closed, :chevrons attr_accessor :level, :progname, :formatter, :local_level @@ -142,7 +154,7 @@ module ActiveSupport @chevrons << x end - def add(message_level, message=nil, progname=nil, &block) + def add(message_level, message = nil, progname = nil, &block) @adds << [message_level, message, progname] if message_level >= local_level end @@ -150,5 +162,9 @@ module ActiveSupport @closed = true end end + + class FakeLogger < CustomLogger + include LoggerSilence + end end end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 0df4173a1a..f53b98c73e 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -47,6 +47,17 @@ module ActiveSupport assert_raises(RuntimeError) { middleware.call({}) } assert_nil LocalCacheRegistry.cache_for(key) end + + def test_local_cache_cleared_on_throw + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + throw :warden + }) + assert_throws(:warden) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end end end end @@ -316,7 +327,7 @@ module CacheStoreBehavior def test_should_read_and_write_nil assert @cache.write("foo", nil) - assert_equal nil, @cache.read("foo") + assert_nil @cache.read("foo") end def test_should_read_and_write_false @@ -464,7 +475,7 @@ module CacheStoreBehavior Time.stub(:now, Time.at(time)) do result = @cache.fetch("foo") do - assert_equal nil, @cache.read("foo") + assert_nil @cache.read("foo") "baz" end assert_equal "baz", result @@ -476,7 +487,7 @@ module CacheStoreBehavior @cache.write("foo", "bar", expires_in: 60) Time.stub(:now, time + 71) do result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal nil, @cache.read("foo") + assert_nil @cache.read("foo") "baz" end assert_equal "baz", result @@ -566,11 +577,92 @@ module CacheStoreBehavior ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end +end - def test_can_call_deprecated_namesaced_key - assert_deprecated "`namespaced_key` is deprecated" do - @cache.send(:namespaced_key, 111, {}) - end +module CacheStoreVersionBehavior + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def test_fetch_with_right_version_should_hit + @cache.fetch("foo", version: 1) { "bar" } + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_fetch_with_wrong_version_should_miss + @cache.fetch("foo", version: 1) { "bar" } + assert_nil @cache.read("foo", version: 2) + end + + def test_read_with_right_version_should_hit + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_read_with_wrong_version_should_miss + @cache.write("foo", "bar", version: 1) + assert_nil @cache.read("foo", version: 2) + end + + def test_exist_with_right_version_should_be_true + @cache.write("foo", "bar", version: 1) + assert @cache.exist?("foo", version: 1) + end + + def test_exist_with_wrong_version_should_be_false + @cache.write("foo", "bar", version: 1) + assert !@cache.exist?("foo", version: 2) + end + + def test_reading_and_writing_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert_equal "bar", @cache.read(m1v1) + assert_nil @cache.read(m1v2) + end + + def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write([ "something", m1v1 ], "bar") + assert_equal "bar", @cache.read([ "something", m1v1 ]) + assert_nil @cache.read([ "something", m1v2 ]) + end + + def test_fetching_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.fetch(m1v1) { "bar" } + assert_equal "bar", @cache.fetch(m1v1) { "bu" } + assert_equal "bu", @cache.fetch(m1v2) { "bu" } + end + + def test_exist_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert @cache.exist?(m1v1) + assert_not @cache.fetch(m1v2) + end + + def test_fetch_multi_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m2v1 = ModelWithKeyAndVersion.new("model/2", 1) + m2v2 = ModelWithKeyAndVersion.new("model/2", 2) + + first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } + second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } + + assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) + assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) + end + + def test_version_is_normalized + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: "1") end end @@ -681,9 +773,9 @@ module LocalCacheBehavior def test_local_cache_of_read_nil @cache.with_local_cache do - assert_equal nil, @cache.read("foo") + assert_nil @cache.read("foo") @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } - assert_equal nil, @cache.read("foo") + assert_nil @cache.read("foo") end end @@ -703,6 +795,14 @@ module LocalCacheBehavior end end + def test_local_cache_of_write_with_unless_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.write("foo", "baz", unless_exist: true) + assert_equal @peek.read("foo"), @cache.read("foo") + end + end + def test_local_cache_of_delete @cache.with_local_cache do @cache.write("foo", "bar") @@ -747,15 +847,6 @@ module LocalCacheBehavior app = @cache.middleware.new(app) app.call({}) end - - def test_can_call_deprecated_set_cache_value - @cache.with_local_cache do - assert_deprecated "`set_cache_value` is deprecated" do - @cache.send(:set_cache_value, 1, "foo", :ignored, {}) - end - assert_equal 1, @cache.read("foo") - end - end end module AutoloadingCacheBehavior @@ -818,6 +909,7 @@ class FileStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include LocalCacheBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior @@ -838,8 +930,8 @@ class FileStoreTest < ActiveSupport::TestCase end def test_long_uri_encoded_keys - @cache.write("%"*870, 1) - assert_equal 1, @cache.read("%"*870) + @cache.write("%" * 870, 1) + assert_equal 1, @cache.read("%" * 870) end def test_key_transformation @@ -859,7 +951,7 @@ class FileStoreTest < ActiveSupport::TestCase key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" path = @cache.send(:normalize_key, key, {}) Dir::Tmpname.create(path) do |tmpname, n, opts| - assert File.basename(tmpname+".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname+'.lock').length}" + assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" end end @@ -918,12 +1010,6 @@ class FileStoreTest < ActiveSupport::TestCase @cache.write(1, nil) assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) end - - def test_can_call_deprecated_key_file_path - assert_deprecated "`key_file_path` is deprecated" do - assert_equal 111, @cache.send(:key_file_path, 111) - end - end end class MemoryStoreTest < ActiveSupport::TestCase @@ -933,6 +1019,7 @@ class MemoryStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior @@ -1004,7 +1091,7 @@ class MemoryStoreTest < ActiveSupport::TestCase end def test_pruning_is_capped_at_a_max_time - def @cache.delete_entry (*args) + def @cache.delete_entry(*args) sleep(0.01) super end @@ -1054,6 +1141,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include LocalCacheBehavior include CacheIncrementDecrementBehavior include EncodedKeyCacheBehavior @@ -1098,12 +1186,6 @@ class MemCacheStoreTest < ActiveSupport::TestCase value << "bingo" assert_not_equal value, @cache.read("foo") end - - def test_can_call_deprecated_escape_key - assert_deprecated "`escape_key` is deprecated" do - assert_equal 111, @cache.send(:escape_key, 111) - end - end end class NullStoreTest < ActiveSupport::TestCase diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index b4e98edd84..4f00afb581 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -23,10 +23,6 @@ module CallbacksTest method_name end - def callback_string(callback_method) - "history << [#{callback_method.to_sym.inspect}, :string]" - end - def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end @@ -56,10 +52,11 @@ module CallbacksTest end class Person < Record + attr_accessor :save_fails + [:before_save, :after_save].each do |callback_method| callback_method_sym = callback_method.to_sym send(callback_method, callback_symbol(callback_method_sym)) - ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method_sym)) } send(callback_method, callback_proc(callback_method_sym)) send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, ""))) send(callback_method, CallbackClass) @@ -67,7 +64,9 @@ module CallbacksTest end def save - run_callbacks :save + run_callbacks :save do + raise "inside save" if save_fails + end end end @@ -193,10 +192,12 @@ module CallbacksTest before_save Proc.new { |r| r.history << [:before_save, :symbol] }, unless: :no before_save Proc.new { |r| r.history << "b00m" }, unless: :yes # string - before_save Proc.new { |r| r.history << [:before_save, :string] }, if: "yes" - before_save Proc.new { |r| r.history << "b00m" }, if: "no" - before_save Proc.new { |r| r.history << [:before_save, :string] }, unless: "no" - before_save Proc.new { |r| r.history << "b00m" }, unless: "yes" + ActiveSupport::Deprecation.silence do + before_save Proc.new { |r| r.history << [:before_save, :string] }, if: "yes" + before_save Proc.new { |r| r.history << "b00m" }, if: "no" + before_save Proc.new { |r| r.history << [:before_save, :string] }, unless: "no" + before_save Proc.new { |r| r.history << "b00m" }, unless: "yes" + end # Combined if and unless before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, if: :yes, unless: :no before_save Proc.new { |r| r.history << "b00m" }, if: :yes, unless: :yes @@ -220,13 +221,54 @@ module CallbacksTest define_callbacks :save end - class AroundPerson < MySuper + class MySlate < MySuper attr_reader :history + attr_accessor :save_fails + + def initialize + @history = [] + end + + def save + run_callbacks :save do + raise "inside save" if save_fails + @history << "running" + end + end + + def no; false; end + def yes; true; end + + def method_missing(sym, *) + case sym + when /^log_(.*)/ + @history << $1 + nil + when /^wrap_(.*)/ + @history << "wrap_#$1" + yield + @history << "unwrap_#$1" + nil + when /^double_(.*)/ + @history << "first_#$1" + yield + @history << "second_#$1" + yield + @history << "third_#$1" + else + super + end + end + + def respond_to_missing?(sym) + sym =~ /^(log|wrap)_/ || super + end + end + class AroundPerson < MySlate set_callback :save, :before, :nope, if: :no set_callback :save, :before, :nope, unless: :yes set_callback :save, :after, :tweedle - ActiveSupport::Deprecation.silence { set_callback :save, :before, "tweedle_dee" } set_callback :save, :before, proc { |m| m.history << "yup" } set_callback :save, :before, :nope, if: proc { false } set_callback :save, :before, :nope, unless: proc { true } @@ -237,9 +279,6 @@ module CallbacksTest set_callback :save, :around, :w0tno, if: :no set_callback :save, :around, :tweedle_deedle - def no; false; end - def yes; true; end - def nope @history << "boom" end @@ -259,10 +298,6 @@ module CallbacksTest yield end - def tweedle_dee - @history << "tweedle dee" - end - def tweedle_dum @history << "tweedle dum pre" yield @@ -278,16 +313,6 @@ module CallbacksTest yield @history << "tweedle deedle post" end - - def initialize - @history = [] - end - - def save - run_callbacks :save do - @history << "running" - end - end end class AroundPersonResult < MySuper @@ -388,7 +413,6 @@ module CallbacksTest around = AroundPerson.new around.save assert_equal [ - "tweedle dee", "yup", "yup", "tweedle dum pre", "w0tyes before", @@ -402,6 +426,97 @@ module CallbacksTest end end + class DoubleYieldTest < ActiveSupport::TestCase + class DoubleYieldModel < MySlate + set_callback :save, :around, :wrap_outer + set_callback :save, :around, :double_trouble + set_callback :save, :around, :wrap_inner + end + + def test_double_save + double = DoubleYieldModel.new + double.save + assert_equal [ + "wrap_outer", + "first_trouble", + "wrap_inner", + "running", + "unwrap_inner", + "second_trouble", + "wrap_inner", + "running", + "unwrap_inner", + "third_trouble", + "unwrap_outer", + ], double.history + end + end + + class CallStackTest < ActiveSupport::TestCase + def test_tidy_call_stack + around = AroundPerson.new + around.save_fails = true + + exception = (around.save rescue $!) + + # Make sure we have the exception we're expecting + assert_equal "inside save", exception.message + + call_stack = exception.backtrace_locations + call_stack.pop caller_locations(0).size + + # Yes, this looks like an implementation test, but it's the least + # obtuse way of asserting that there aren't a load of entries in + # the call stack for each callback. + # + # If you've renamed a method, or squeezed more lines out, go ahead + # and update this assertion. But if you're here because a + # refactoring added new lines, please reconsider. + + # As shown here, our current budget is one line for run_callbacks + # itself, plus N+1 lines where N is the number of :around + # callbacks that have been invoked, if there are any (plus + # whatever the callbacks do themselves, of course). + + assert_equal [ + "block in save", + "block in run_callbacks", + "tweedle_deedle", + "block in run_callbacks", + "w0tyes", + "block in run_callbacks", + "tweedle_dum", + "block in run_callbacks", + ("call" if RUBY_VERSION < "2.3"), + "run_callbacks", + "save" + ].compact, call_stack.map(&:label) + end + + def test_short_call_stack + person = Person.new + person.save_fails = true + + exception = (person.save rescue $!) + + # Make sure we have the exception we're expecting + assert_equal "inside save", exception.message + + call_stack = exception.backtrace_locations + call_stack.pop caller_locations(0).size + + # This budget much simpler: with no :around callbacks invoked, + # there should be just one line. run_callbacks yields directly + # back to its caller. + + assert_equal [ + "block in save", + "run_callbacks", + "save" + ], call_stack.map(&:label) + end + end + class AroundCallbackResultTest < ActiveSupport::TestCase def test_save_around around = AroundPersonResult.new @@ -416,7 +531,6 @@ module CallbacksTest assert_equal [], person.history person.save assert_equal [ - [:before_save, :string], [:before_save, :proc], [:before_save, :object], [:before_save, :block], @@ -424,7 +538,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], person.history end @@ -443,7 +556,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], person.history end @@ -456,7 +568,6 @@ module CallbacksTest person.save assert_equal [ [:before_save, :symbol], - [:before_save, :string], [:before_save, :proc], [:before_save, :object], [:before_save, :class], @@ -465,7 +576,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], person.history end @@ -759,37 +869,11 @@ module CallbacksTest end end - class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase - def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set - obj = CallbackFalseTerminator.new - obj.save - assert_equal nil, obj.halted - assert obj.saved - end - end - - class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase - def setup - ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true - end - - def test_returning_false_does_not_halt_callback_if_config_variable_is_true + class CallbackFalseTerminatorTest < ActiveSupport::TestCase + def test_returning_false_does_not_halt_callback obj = CallbackFalseTerminator.new obj.save - assert_equal nil, obj.halted - assert obj.saved - end - end - - class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase - def setup - ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false - end - - def test_returning_false_does_not_halt_callback_if_config_variable_is_false - obj = CallbackFalseTerminator.new - obj.save - assert_equal nil, obj.halted + assert_nil obj.halted assert obj.saved end end @@ -815,7 +899,6 @@ module CallbacksTest writer.save assert_equal [ [:before_save, :symbol], - [:before_save, :string], [:before_save, :proc], [:before_save, :object], [:before_save, :class], @@ -824,7 +907,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], writer.history end @@ -879,7 +961,7 @@ module CallbacksTest def test_proc_arity_2 assert_raises(ArgumentError) do - klass = build_class(->(x,y) {}) + klass = build_class(->(x, y) {}) klass.new.run end end @@ -916,7 +998,7 @@ module CallbacksTest set_callback :foo, :before, :foo, if: callback def run; run_callbacks :foo; end private - def foo; end + def foo; end } object = klass.new object.run @@ -958,7 +1040,7 @@ module CallbacksTest def test_proc_arity2 assert_raises(ArgumentError) do - object = build_class(->(a,b) {}).new + object = build_class(->(a, b) {}).new object.run end end @@ -1038,14 +1120,6 @@ module CallbacksTest assert_equal 1, calls.length end - def test_add_eval - calls = [] - klass = ActiveSupport::Deprecation.silence { build_class("bar") } - klass.class_eval { define_method(:bar) { calls << klass } } - klass.new.run - assert_equal 1, calls.length - end - def test_skip_class # removes one at a time calls = [] callback = Class.new { @@ -1080,7 +1154,7 @@ module CallbacksTest def test_skip_string # raises error calls = [] - klass = ActiveSupport::Deprecation.silence { build_class("bar") } + klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } assert_raises(ArgumentError) { klass.skip "bar" } klass.new.run @@ -1107,11 +1181,22 @@ module CallbacksTest end class DeprecatedWarningTest < ActiveSupport::TestCase - def test_deprecate_string_callback + def test_deprecate_string_conditional_options klass = Class.new(Record) - assert_deprecated do - klass.send :before_save, "tweedle_dee" + assert_deprecated { klass.before_save :tweedle, if: "true" } + assert_deprecated { klass.after_save :tweedle, unless: "false" } + assert_deprecated { klass.skip_callback :save, :before, :tweedle, if: "true" } + assert_deprecated { klass.skip_callback :save, :after, :tweedle, unless: "false" } + end + end + + class NotPermittedStringCallbackTest < ActiveSupport::TestCase + def test_passing_string_callback_is_not_permitted + klass = Class.new(Record) + + assert_raises(ArgumentError) do + klass.before_save "tweedle" end end end diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb index c618fea81a..004b4dc9ce 100644 --- a/activesupport/test/class_cache_test.rb +++ b/activesupport/test/class_cache_test.rb @@ -61,7 +61,7 @@ module ActiveSupport def test_safe_get_constantizes_doesnt_fail_on_invalid_names assert @cache.empty? - assert_equal nil, @cache.safe_get("OmgTotallyInvalidConstantName") + assert_nil @cache.safe_get("OmgTotallyInvalidConstantName") end def test_new_rejects_strings diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 95507c815d..7a5a5414a7 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -76,7 +76,7 @@ class ConcernTest < ActiveSupport::TestCase end def test_class_methods_are_extended_only_on_expected_objects - ::Object.__send__(:include, Qux) + ::Object.include(Qux) Object.extend(Qux::ClassMethods) # module needs to be created after Qux is included in Object or bug won't # be triggered diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index af2db8c991..32b720bcbb 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -73,6 +73,11 @@ module ConstantizeTestCases yield("RaisesNoMethodError") end end + + with_autoloading_fixtures do + yield("Prepend::SubClassConflict") + assert_equal "constant", defined?(Prepend::SubClassConflict) + end end def run_safe_constantize_tests_on diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb index 469efcd934..29e661a99b 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -25,11 +25,6 @@ class ToSentenceTest < ActiveSupport::TestCase assert_equal "one, two and three", ["one", "two", "three"].to_sentence(last_word_connector: " and ") end - def test_to_sentence_with_fallback_string - assert_equal "none", [].to_sentence(fallback_string: "none") - assert_equal "one, two, and three", ["one", "two", "three"].to_sentence(fallback_string: "none") - end - def test_two_elements assert_equal "one and two", ["one", "two"].to_sentence assert_equal "one two", ["one", "two"].to_sentence(two_words_connector: " ") @@ -63,7 +58,7 @@ class ToSentenceTest < ActiveSupport::TestCase ["one", "two"].to_sentence(passing: "invalid option") end - assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale, :fallback_string" + assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" end def test_always_returns_string diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb index 86c9bae131..4c6aadba8c 100644 --- a/activesupport/test/core_ext/array/grouping_test.rb +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -4,11 +4,11 @@ require "active_support/core_ext/array" class GroupingTest < ActiveSupport::TestCase def setup # In Ruby < 2.4, test we avoid Integer#/ (redefined by mathn) - Fixnum.send :private, :/ unless Fixnum == Integer + Fixnum.send :private, :/ unless 0.class == Integer end def teardown - Fixnum.send :public, :/ unless Fixnum == Integer + Fixnum.send :public, :/ unless 0.class == Integer end def test_in_groups_of_with_perfect_fit @@ -114,7 +114,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/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb index 5a9ec78cc1..f16043c612 100644 --- a/activesupport/test/core_ext/class/attribute_test.rb +++ b/activesupport/test/core_ext/class/attribute_test.rb @@ -3,7 +3,11 @@ require "active_support/core_ext/class/attribute" class ClassAttributeTest < ActiveSupport::TestCase def setup - @klass = Class.new { class_attribute :setting } + @klass = Class.new do + class_attribute :setting + class_attribute :timeout, default: 5 + end + @sub = Class.new(@klass) end @@ -12,6 +16,10 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_nil @sub.setting end + test "custom default" do + assert_equal 5, @klass.timeout + end + test "inheritable" do @klass.setting = 1 assert_equal 1, @sub.setting diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb index a9c44907cc..a7905196ae 100644 --- a/activesupport/test/core_ext/class_test.rb +++ b/activesupport/test/core_ext/class_test.rb @@ -25,4 +25,14 @@ class ClassTest < ActiveSupport::TestCase assert_equal [Baz], Bar.subclasses assert_equal [], Baz.subclasses end + + def test_descendants_excludes_singleton_classes + klass = Parent.new.singleton_class + refute 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" + 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 bf83ac602f..6c77e8f313 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -2,106 +2,106 @@ require "abstract_unit" module DateAndTimeBehavior def test_yesterday - assert_equal date_time_init(2005,2,21,10,10,10), date_time_init(2005,2,22,10,10,10).yesterday - assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).yesterday.yesterday + assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).yesterday + assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 3, 2, 10, 10, 10).yesterday.yesterday end def test_prev_day - assert_equal date_time_init(2005,2,21,10,10,10), date_time_init(2005,2,22,10,10,10).prev_day - assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).prev_day.prev_day + assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day + assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 3, 2, 10, 10, 10).prev_day.prev_day end def test_tomorrow - assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).tomorrow - assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).tomorrow.tomorrow + assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).tomorrow + assert_equal date_time_init(2005, 3, 2, 10, 10, 10), date_time_init(2005, 2, 28, 10, 10, 10).tomorrow.tomorrow end def test_next_day - assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).next_day - assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).next_day.next_day + assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day + assert_equal date_time_init(2005, 3, 2, 10, 10, 10), date_time_init(2005, 2, 28, 10, 10, 10).next_day.next_day end def test_days_ago - assert_equal date_time_init(2005,6,4,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(1) - assert_equal date_time_init(2005,5,31,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(5) + assert_equal date_time_init(2005, 6, 4, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).days_ago(1) + assert_equal date_time_init(2005, 5, 31, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).days_ago(5) end def test_days_since - assert_equal date_time_init(2005,6,6,10,10,10), date_time_init(2005,6,5,10,10,10).days_since(1) - assert_equal date_time_init(2005,1,1,10,10,10), date_time_init(2004,12,31,10,10,10).days_since(1) + assert_equal date_time_init(2005, 6, 6, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).days_since(1) + assert_equal date_time_init(2005, 1, 1, 10, 10, 10), date_time_init(2004, 12, 31, 10, 10, 10).days_since(1) end def test_weeks_ago - assert_equal date_time_init(2005,5,29,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(1) - assert_equal date_time_init(2005,5,1,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(5) - assert_equal date_time_init(2005,4,24,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(6) - assert_equal date_time_init(2005,2,27,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(14) - assert_equal date_time_init(2004,12,25,10,10,10), date_time_init(2005,1,1,10,10,10).weeks_ago(1) + assert_equal date_time_init(2005, 5, 29, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(1) + assert_equal date_time_init(2005, 5, 1, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(5) + assert_equal date_time_init(2005, 4, 24, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(6) + assert_equal date_time_init(2005, 2, 27, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(14) + assert_equal date_time_init(2004, 12, 25, 10, 10, 10), date_time_init(2005, 1, 1, 10, 10, 10).weeks_ago(1) end def test_weeks_since - assert_equal date_time_init(2005,7,14,10,10,10), date_time_init(2005,7,7,10,10,10).weeks_since(1) - assert_equal date_time_init(2005,7,14,10,10,10), date_time_init(2005,7,7,10,10,10).weeks_since(1) - assert_equal date_time_init(2005,7,4,10,10,10), date_time_init(2005,6,27,10,10,10).weeks_since(1) - assert_equal date_time_init(2005,1,4,10,10,10), date_time_init(2004,12,28,10,10,10).weeks_since(1) + assert_equal date_time_init(2005, 7, 14, 10, 10, 10), date_time_init(2005, 7, 7, 10, 10, 10).weeks_since(1) + assert_equal date_time_init(2005, 7, 14, 10, 10, 10), date_time_init(2005, 7, 7, 10, 10, 10).weeks_since(1) + assert_equal date_time_init(2005, 7, 4, 10, 10, 10), date_time_init(2005, 6, 27, 10, 10, 10).weeks_since(1) + assert_equal date_time_init(2005, 1, 4, 10, 10, 10), date_time_init(2004, 12, 28, 10, 10, 10).weeks_since(1) end def test_months_ago - assert_equal date_time_init(2005,5,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(1) - assert_equal date_time_init(2004,11,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(7) - assert_equal date_time_init(2004,12,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(6) - assert_equal date_time_init(2004,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(12) - assert_equal date_time_init(2003,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(24) + assert_equal date_time_init(2005, 5, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(1) + assert_equal date_time_init(2004, 11, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(7) + assert_equal date_time_init(2004, 12, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(6) + assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(12) + assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(24) end def test_months_since - assert_equal date_time_init(2005,7,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(1) - assert_equal date_time_init(2006,1,5,10,10,10), date_time_init(2005,12,5,10,10,10).months_since(1) - assert_equal date_time_init(2005,12,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(6) - assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,12,5,10,10,10).months_since(6) - assert_equal date_time_init(2006,1,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(7) - assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(12) - assert_equal date_time_init(2007,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(24) - assert_equal date_time_init(2005,4,30,10,10,10), date_time_init(2005,3,31,10,10,10).months_since(1) - assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,1,29,10,10,10).months_since(1) - assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,1,30,10,10,10).months_since(1) - assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,1,31,10,10,10).months_since(1) + assert_equal date_time_init(2005, 7, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(1) + assert_equal date_time_init(2006, 1, 5, 10, 10, 10), date_time_init(2005, 12, 5, 10, 10, 10).months_since(1) + assert_equal date_time_init(2005, 12, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(6) + assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 12, 5, 10, 10, 10).months_since(6) + assert_equal date_time_init(2006, 1, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(7) + assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(12) + assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(24) + assert_equal date_time_init(2005, 4, 30, 10, 10, 10), date_time_init(2005, 3, 31, 10, 10, 10).months_since(1) + assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 1, 29, 10, 10, 10).months_since(1) + assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 1, 30, 10, 10, 10).months_since(1) + assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 1, 31, 10, 10, 10).months_since(1) end def test_years_ago - assert_equal date_time_init(2004,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_ago(1) - assert_equal date_time_init(1998,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_ago(7) - assert_equal date_time_init(2003,2,28,10,10,10), date_time_init(2004,2,29,10,10,10).years_ago(1) # 1 year ago from leap day + assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_ago(1) + assert_equal date_time_init(1998, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_ago(7) + assert_equal date_time_init(2003, 2, 28, 10, 10, 10), date_time_init(2004, 2, 29, 10, 10, 10).years_ago(1) # 1 year ago from leap day end def test_years_since - assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_since(1) - assert_equal date_time_init(2012,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_since(7) - assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2004,2,29,10,10,10).years_since(1) # 1 year since leap day - assert_equal date_time_init(2182,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_since(177) + assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_since(1) + assert_equal date_time_init(2012, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_since(7) + assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2004, 2, 29, 10, 10, 10).years_since(1) # 1 year since leap day + assert_equal date_time_init(2182, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_since(177) end def test_beginning_of_month - assert_equal date_time_init(2005,2,1,0,0,0), date_time_init(2005,2,22,10,10,10).beginning_of_month + assert_equal date_time_init(2005, 2, 1, 0, 0, 0), date_time_init(2005, 2, 22, 10, 10, 10).beginning_of_month end def test_beginning_of_quarter - assert_equal date_time_init(2005,1,1,0,0,0), date_time_init(2005,2,15,10,10,10).beginning_of_quarter - assert_equal date_time_init(2005,1,1,0,0,0), date_time_init(2005,1,1,0,0,0).beginning_of_quarter - assert_equal date_time_init(2005,10,1,0,0,0), date_time_init(2005,12,31,10,10,10).beginning_of_quarter - assert_equal date_time_init(2005,4,1,0,0,0), date_time_init(2005,6,30,23,59,59).beginning_of_quarter + assert_equal date_time_init(2005, 1, 1, 0, 0, 0), date_time_init(2005, 2, 15, 10, 10, 10).beginning_of_quarter + assert_equal date_time_init(2005, 1, 1, 0, 0, 0), date_time_init(2005, 1, 1, 0, 0, 0).beginning_of_quarter + assert_equal date_time_init(2005, 10, 1, 0, 0, 0), date_time_init(2005, 12, 31, 10, 10, 10).beginning_of_quarter + assert_equal date_time_init(2005, 4, 1, 0, 0, 0), date_time_init(2005, 6, 30, 23, 59, 59).beginning_of_quarter end def test_end_of_quarter - assert_equal date_time_init(2007,3,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,2,15,10,10,10).end_of_quarter - assert_equal date_time_init(2007,3,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,3,31,0,0,0).end_of_quarter - assert_equal date_time_init(2007,12,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,12,21,10,10,10).end_of_quarter - assert_equal date_time_init(2007,6,30,23,59,59,Rational(999999999, 1000)), date_time_init(2007,4,1,0,0,0).end_of_quarter - assert_equal date_time_init(2008,6,30,23,59,59,Rational(999999999, 1000)), date_time_init(2008,5,31,0,0,0).end_of_quarter + assert_equal date_time_init(2007, 3, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 2, 15, 10, 10, 10).end_of_quarter + assert_equal date_time_init(2007, 3, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 3, 31, 0, 0, 0).end_of_quarter + assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 21, 10, 10, 10).end_of_quarter + assert_equal date_time_init(2007, 6, 30, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 4, 1, 0, 0, 0).end_of_quarter + assert_equal date_time_init(2008, 6, 30, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2008, 5, 31, 0, 0, 0).end_of_quarter end def test_beginning_of_year - assert_equal date_time_init(2005,1,1,0,0,0), date_time_init(2005,2,22,10,10,10).beginning_of_year + assert_equal date_time_init(2005, 1, 1, 0, 0, 0), date_time_init(2005, 2, 22, 10, 10, 10).beginning_of_year end def test_next_week @@ -110,10 +110,10 @@ module DateAndTimeBehavior # | 22/2 | | | | | # | | | | 4/3 | | # friday in next week `next_week(:friday)` # 23/10 | | | | | | # 30/10 | | | | | | # monday in next week `next_week` # 23/10 | | | | | | # | | 1/11 | | | | # wednesday in next week `next_week(:wednesday)` - assert_equal date_time_init(2005,2,28,0,0,0), date_time_init(2005,2,22,15,15,10).next_week - assert_equal date_time_init(2005,3,4,0,0,0), date_time_init(2005,2,22,15,15,10).next_week(:friday) - assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week - assert_equal date_time_init(2006,11,1,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:wednesday) + assert_equal date_time_init(2005, 2, 28, 0, 0, 0), date_time_init(2005, 2, 22, 15, 15, 10).next_week + assert_equal date_time_init(2005, 3, 4, 0, 0, 0), date_time_init(2005, 2, 22, 15, 15, 10).next_week(:friday) + assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week + assert_equal date_time_init(2006, 11, 1, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week(:wednesday) end def test_next_week_with_default_beginning_of_week_set @@ -126,45 +126,47 @@ module DateAndTimeBehavior end def test_next_week_at_same_time - assert_equal date_time_init(2005,2,28,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:monday, same_time: true) - assert_equal date_time_init(2005,3,4,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:friday, same_time: true) - assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:monday, same_time: true) - assert_equal date_time_init(2006,11,1,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:wednesday, same_time: true) + assert_equal date_time_init(2005, 2, 28, 15, 15, 10), date_time_init(2005, 2, 22, 15, 15, 10).next_week(:monday, same_time: true) + assert_equal date_time_init(2005, 2, 28, 15, 15, 10, 999999), date_time_init(2005, 2, 22, 15, 15, 10, 999999).next_week(:monday, same_time: true) + assert_equal date_time_init(2005, 2, 28, 15, 15, 10, Rational(999999999, 1000)), date_time_init(2005, 2, 22, 15, 15, 10, Rational(999999999, 1000)).next_week(:monday, same_time: true) + assert_equal date_time_init(2005, 3, 4, 15, 15, 10), date_time_init(2005, 2, 22, 15, 15, 10).next_week(:friday, same_time: true) + assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week(:monday, same_time: true) + assert_equal date_time_init(2006, 11, 1, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week(:wednesday, same_time: true) end def test_next_weekday_on_wednesday - assert_equal date_time_init(2015,1,8,0,0,0), date_time_init(2015,1,7,0,0,0).next_weekday - assert_equal date_time_init(2015,1,8,15,15,10), date_time_init(2015,1,7,15,15,10).next_weekday + assert_equal date_time_init(2015, 1, 8, 0, 0, 0), date_time_init(2015, 1, 7, 0, 0, 0).next_weekday + assert_equal date_time_init(2015, 1, 8, 15, 15, 10), date_time_init(2015, 1, 7, 15, 15, 10).next_weekday end def test_next_weekday_on_friday - assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,2,0,0,0).next_weekday - assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,2,15,15,10).next_weekday + assert_equal date_time_init(2015, 1, 5, 0, 0, 0), date_time_init(2015, 1, 2, 0, 0, 0).next_weekday + assert_equal date_time_init(2015, 1, 5, 15, 15, 10), date_time_init(2015, 1, 2, 15, 15, 10).next_weekday end def test_next_weekday_on_saturday - assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,3,0,0,0).next_weekday - assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,3,15,15,10).next_weekday + assert_equal date_time_init(2015, 1, 5, 0, 0, 0), date_time_init(2015, 1, 3, 0, 0, 0).next_weekday + assert_equal date_time_init(2015, 1, 5, 15, 15, 10), date_time_init(2015, 1, 3, 15, 15, 10).next_weekday end def test_next_month_on_31st - assert_equal date_time_init(2005,9,30,15,15,10), date_time_init(2005,8,31,15,15,10).next_month + assert_equal date_time_init(2005, 9, 30, 15, 15, 10), date_time_init(2005, 8, 31, 15, 15, 10).next_month end def test_next_quarter_on_31st - assert_equal date_time_init(2005,11,30,15,15,10), date_time_init(2005,8,31,15,15,10).next_quarter + assert_equal date_time_init(2005, 11, 30, 15, 15, 10), date_time_init(2005, 8, 31, 15, 15, 10).next_quarter end def test_next_year - assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).next_year + assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year end def test_prev_week - assert_equal date_time_init(2005,2,21,0,0,0), date_time_init(2005,3,1,15,15,10).prev_week - assert_equal date_time_init(2005,2,22,0,0,0), date_time_init(2005,3,1,15,15,10).prev_week(:tuesday) - assert_equal date_time_init(2005,2,25,0,0,0), date_time_init(2005,3,1,15,15,10).prev_week(:friday) - assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,11,6,0,0,0).prev_week - assert_equal date_time_init(2006,11,15,0,0,0), date_time_init(2006,11,23,0,0,0).prev_week(:wednesday) + assert_equal date_time_init(2005, 2, 21, 0, 0, 0), date_time_init(2005, 3, 1, 15, 15, 10).prev_week + assert_equal date_time_init(2005, 2, 22, 0, 0, 0), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:tuesday) + assert_equal date_time_init(2005, 2, 25, 0, 0, 0), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:friday) + assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 11, 6, 0, 0, 0).prev_week + assert_equal date_time_init(2006, 11, 15, 0, 0, 0), date_time_init(2006, 11, 23, 0, 0, 0).prev_week(:wednesday) end def test_prev_week_with_default_beginning_of_week @@ -177,138 +179,138 @@ module DateAndTimeBehavior end def test_prev_week_at_same_time - assert_equal date_time_init(2005,2,21,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:monday, same_time: true) - assert_equal date_time_init(2005,2,22,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:tuesday, same_time: true) - assert_equal date_time_init(2005,2,25,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:friday, same_time: true) - assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,11,6,0,0,0).prev_week(:monday, same_time: true) - assert_equal date_time_init(2006,11,15,0,0,0), date_time_init(2006,11,23,0,0,0).prev_week(:wednesday, same_time: true) + assert_equal date_time_init(2005, 2, 21, 15, 15, 10), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:monday, same_time: true) + assert_equal date_time_init(2005, 2, 22, 15, 15, 10), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:tuesday, same_time: true) + assert_equal date_time_init(2005, 2, 25, 15, 15, 10), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:friday, same_time: true) + assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 11, 6, 0, 0, 0).prev_week(:monday, same_time: true) + assert_equal date_time_init(2006, 11, 15, 0, 0, 0), date_time_init(2006, 11, 23, 0, 0, 0).prev_week(:wednesday, same_time: true) end def test_prev_weekday_on_wednesday - assert_equal date_time_init(2015,1,6,0,0,0), date_time_init(2015,1,7,0,0,0).prev_weekday - assert_equal date_time_init(2015,1,6,15,15,10), date_time_init(2015,1,7,15,15,10).prev_weekday + assert_equal date_time_init(2015, 1, 6, 0, 0, 0), date_time_init(2015, 1, 7, 0, 0, 0).prev_weekday + assert_equal date_time_init(2015, 1, 6, 15, 15, 10), date_time_init(2015, 1, 7, 15, 15, 10).prev_weekday end def test_prev_weekday_on_monday - assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,5,0,0,0).prev_weekday - assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,5,15,15,10).prev_weekday + assert_equal date_time_init(2015, 1, 2, 0, 0, 0), date_time_init(2015, 1, 5, 0, 0, 0).prev_weekday + assert_equal date_time_init(2015, 1, 2, 15, 15, 10), date_time_init(2015, 1, 5, 15, 15, 10).prev_weekday end def test_prev_weekday_on_sunday - assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,4,0,0,0).prev_weekday - assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,4,15,15,10).prev_weekday + assert_equal date_time_init(2015, 1, 2, 0, 0, 0), date_time_init(2015, 1, 4, 0, 0, 0).prev_weekday + assert_equal date_time_init(2015, 1, 2, 15, 15, 10), date_time_init(2015, 1, 4, 15, 15, 10).prev_weekday end def test_prev_month_on_31st - assert_equal date_time_init(2004,2,29,10,10,10), date_time_init(2004,3,31,10,10,10).prev_month + assert_equal date_time_init(2004, 2, 29, 10, 10, 10), date_time_init(2004, 3, 31, 10, 10, 10).prev_month end def test_prev_quarter_on_31st - assert_equal date_time_init(2004,2,29,10,10,10), date_time_init(2004,5,31,10,10,10).prev_quarter + assert_equal date_time_init(2004, 2, 29, 10, 10, 10), date_time_init(2004, 5, 31, 10, 10, 10).prev_quarter end def test_prev_year - assert_equal date_time_init(2004,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).prev_year + assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year end def test_days_to_week_start - assert_equal 0, date_time_init(2011,11,01,0,0,0).days_to_week_start(:tuesday) - assert_equal 1, date_time_init(2011,11,02,0,0,0).days_to_week_start(:tuesday) - assert_equal 2, date_time_init(2011,11,03,0,0,0).days_to_week_start(:tuesday) - assert_equal 3, date_time_init(2011,11,04,0,0,0).days_to_week_start(:tuesday) - assert_equal 4, date_time_init(2011,11,05,0,0,0).days_to_week_start(:tuesday) - assert_equal 5, date_time_init(2011,11,06,0,0,0).days_to_week_start(:tuesday) - assert_equal 6, date_time_init(2011,11,07,0,0,0).days_to_week_start(:tuesday) - - assert_equal 3, date_time_init(2011,11,03,0,0,0).days_to_week_start(:monday) - assert_equal 3, date_time_init(2011,11,04,0,0,0).days_to_week_start(:tuesday) - assert_equal 3, date_time_init(2011,11,05,0,0,0).days_to_week_start(:wednesday) - assert_equal 3, date_time_init(2011,11,06,0,0,0).days_to_week_start(:thursday) - assert_equal 3, date_time_init(2011,11,07,0,0,0).days_to_week_start(:friday) - assert_equal 3, date_time_init(2011,11,8,0,0,0).days_to_week_start(:saturday) - assert_equal 3, date_time_init(2011,11,9,0,0,0).days_to_week_start(:sunday) + assert_equal 0, date_time_init(2011, 11, 01, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 1, date_time_init(2011, 11, 02, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 2, date_time_init(2011, 11, 03, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 3, date_time_init(2011, 11, 04, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 4, date_time_init(2011, 11, 05, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 5, date_time_init(2011, 11, 06, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 6, date_time_init(2011, 11, 07, 0, 0, 0).days_to_week_start(:tuesday) + + assert_equal 3, date_time_init(2011, 11, 03, 0, 0, 0).days_to_week_start(:monday) + assert_equal 3, date_time_init(2011, 11, 04, 0, 0, 0).days_to_week_start(:tuesday) + assert_equal 3, date_time_init(2011, 11, 05, 0, 0, 0).days_to_week_start(:wednesday) + assert_equal 3, date_time_init(2011, 11, 06, 0, 0, 0).days_to_week_start(:thursday) + assert_equal 3, date_time_init(2011, 11, 07, 0, 0, 0).days_to_week_start(:friday) + assert_equal 3, date_time_init(2011, 11, 8, 0, 0, 0).days_to_week_start(:saturday) + assert_equal 3, date_time_init(2011, 11, 9, 0, 0, 0).days_to_week_start(:sunday) end def test_days_to_week_start_with_default_set with_bw_default(:friday) do - assert_equal 6, Time.local(2012,03,8,0,0,0).days_to_week_start - assert_equal 5, Time.local(2012,03,7,0,0,0).days_to_week_start - assert_equal 4, Time.local(2012,03,6,0,0,0).days_to_week_start - assert_equal 3, Time.local(2012,03,5,0,0,0).days_to_week_start - assert_equal 2, Time.local(2012,03,4,0,0,0).days_to_week_start - assert_equal 1, Time.local(2012,03,3,0,0,0).days_to_week_start - assert_equal 0, Time.local(2012,03,2,0,0,0).days_to_week_start + assert_equal 6, Time.local(2012, 03, 8, 0, 0, 0).days_to_week_start + assert_equal 5, Time.local(2012, 03, 7, 0, 0, 0).days_to_week_start + assert_equal 4, Time.local(2012, 03, 6, 0, 0, 0).days_to_week_start + assert_equal 3, Time.local(2012, 03, 5, 0, 0, 0).days_to_week_start + assert_equal 2, Time.local(2012, 03, 4, 0, 0, 0).days_to_week_start + assert_equal 1, Time.local(2012, 03, 3, 0, 0, 0).days_to_week_start + assert_equal 0, Time.local(2012, 03, 2, 0, 0, 0).days_to_week_start end end 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, 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 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(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 end def test_end_of_month - assert_equal date_time_init(2005,3,31,23,59,59,Rational(999999999, 1000)), date_time_init(2005,3,20,10,10,10).end_of_month - assert_equal date_time_init(2005,2,28,23,59,59,Rational(999999999, 1000)), date_time_init(2005,2,20,10,10,10).end_of_month - assert_equal date_time_init(2005,4,30,23,59,59,Rational(999999999, 1000)), date_time_init(2005,4,20,10,10,10).end_of_month + assert_equal date_time_init(2005, 3, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2005, 3, 20, 10, 10, 10).end_of_month + assert_equal date_time_init(2005, 2, 28, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2005, 2, 20, 10, 10, 10).end_of_month + assert_equal date_time_init(2005, 4, 30, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2005, 4, 20, 10, 10, 10).end_of_month end def test_end_of_year - assert_equal date_time_init(2007,12,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,2,22,10,10,10).end_of_year - 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 + assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 2, 22, 10, 10, 10).end_of_year + 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_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 + assert_equal date_time_init(2012, 9, 17, 0, 0, 0), date_time_init(2012, 9, 18, 0, 0, 0).monday end end def test_sunday_with_default_beginning_of_week_set with_bw_default(:wednesday) do - assert_equal date_time_init(2012,9,23,23,59,59, Rational(999999999, 1000)), date_time_init(2012,9,19,0,0,0).sunday + assert_equal date_time_init(2012, 9, 23, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2012, 9, 19, 0, 0, 0).sunday end 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 date_time_init(2015, 1, 3, 0, 0, 0).on_weekend? + assert 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 date_time_init(2015, 1, 4, 0, 0, 0).on_weekend? + assert 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 date_time_init(2015, 1, 5, 0, 0, 0).on_weekend? + assert_not 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 date_time_init(2015, 1, 4, 0, 0, 0).on_weekday? + assert_not 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 date_time_init(2015, 1, 5, 0, 0, 0).on_weekday? + assert 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_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb index 180b3e12aa..6c6205a4d2 100644 --- a/activesupport/test/core_ext/date_and_time_compatibility_test.rb +++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb @@ -16,11 +16,13 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_time_to_time_preserves_timezone with_preserve_timezone(true) do with_env_tz "US/Eastern" do - time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time + source = Time.new(2016, 4, 23, 15, 11, 12, 3600) + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc assert_equal @utc_offset, time.utc_offset + assert_equal source.object_id, time.object_id end end end @@ -28,11 +30,43 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_time_to_time_does_not_preserve_time_zone with_preserve_timezone(false) do with_env_tz "US/Eastern" do - time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time + source = Time.new(2016, 4, 23, 15, 11, 12, 3600) + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc assert_equal @system_offset, time.utc_offset + assert_not_equal source.object_id, time.object_id + end + end + end + + def test_time_to_time_frozen_preserves_timezone + with_preserve_timezone(true) do + with_env_tz "US/Eastern" do + source = Time.new(2016, 4, 23, 15, 11, 12, 3600).freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @utc_offset, time.utc_offset + assert_equal source.object_id, time.object_id + assert_predicate time, :frozen? + end + end + end + + def test_time_to_time_frozen_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz "US/Eastern" do + source = Time.new(2016, 4, 23, 15, 11, 12, 3600).freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @system_offset, time.utc_offset + assert_not_equal source.object_id, time.object_id + assert_not_predicate time, :frozen? end end end @@ -40,7 +74,8 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_datetime_to_time_preserves_timezone with_preserve_timezone(true) do with_env_tz "US/Eastern" do - time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1,24)).to_time + source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)) + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc @@ -52,7 +87,8 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_datetime_to_time_does_not_preserve_time_zone with_preserve_timezone(false) do with_env_tz "US/Eastern" do - time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1,24)).to_time + source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)) + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc @@ -61,17 +97,47 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase end end + def test_datetime_to_time_frozen_preserves_timezone + with_preserve_timezone(true) do + with_env_tz "US/Eastern" do + source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)).freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @utc_offset, time.utc_offset + assert_not_predicate time, :frozen? + end + end + end + + def test_datetime_to_time_frozen_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz "US/Eastern" do + source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)).freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @system_offset, time.utc_offset + assert_not_predicate time, :frozen? + end + end + end + def test_twz_to_time_preserves_timezone with_preserve_timezone(true) do with_env_tz "US/Eastern" do - time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time + source = ActiveSupport::TimeWithZone.new(@utc_time, @zone) + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc assert_instance_of Time, time.getutc assert_equal @utc_offset, time.utc_offset - time = ActiveSupport::TimeWithZone.new(@date_time, @zone).to_time + source = ActiveSupport::TimeWithZone.new(@date_time, @zone) + time = source.to_time assert_instance_of Time, time assert_equal @date_time, time.getutc @@ -84,19 +150,69 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_twz_to_time_does_not_preserve_time_zone with_preserve_timezone(false) do with_env_tz "US/Eastern" do - time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time + source = ActiveSupport::TimeWithZone.new(@utc_time, @zone) + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @system_offset, time.utc_offset + + source = ActiveSupport::TimeWithZone.new(@date_time, @zone) + time = source.to_time + + assert_instance_of Time, time + assert_equal @date_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @system_offset, time.utc_offset + end + end + end + + def test_twz_to_time_frozen_preserves_timezone + with_preserve_timezone(true) do + with_env_tz "US/Eastern" do + source = ActiveSupport::TimeWithZone.new(@utc_time, @zone).freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @utc_offset, time.utc_offset + assert_not_predicate time, :frozen? + + source = ActiveSupport::TimeWithZone.new(@date_time, @zone).freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @date_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @utc_offset, time.utc_offset + assert_not_predicate time, :frozen? + end + end + end + + def test_twz_to_time_frozen_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz "US/Eastern" do + source = ActiveSupport::TimeWithZone.new(@utc_time, @zone).freeze + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc assert_instance_of Time, time.getutc assert_equal @system_offset, time.utc_offset + assert_not_predicate time, :frozen? - time = ActiveSupport::TimeWithZone.new(@date_time, @zone).to_time + source = ActiveSupport::TimeWithZone.new(@date_time, @zone).freeze + time = source.to_time assert_instance_of Time, time assert_equal @date_time, time.getutc assert_instance_of Time, time.getutc assert_equal @system_offset, time.utc_offset + assert_not_predicate time, :frozen? end end end @@ -104,7 +220,8 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_string_to_time_preserves_timezone with_preserve_timezone(true) do with_env_tz "US/Eastern" do - time = "2016-04-23T15:11:12+01:00".to_time + source = "2016-04-23T15:11:12+01:00" + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc @@ -116,11 +233,40 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_string_to_time_does_not_preserve_time_zone with_preserve_timezone(false) do with_env_tz "US/Eastern" do - time = "2016-04-23T15:11:12+01:00".to_time + source = "2016-04-23T15:11:12+01:00" + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @system_offset, time.utc_offset + end + end + end + + def test_string_to_time_frozen_preserves_timezone + with_preserve_timezone(true) do + with_env_tz "US/Eastern" do + source = "2016-04-23T15:11:12+01:00".freeze + time = source.to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @utc_offset, time.utc_offset + assert_not_predicate time, :frozen? + end + end + end + + def test_string_to_time_frozen_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz "US/Eastern" do + source = "2016-04-23T15:11:12+01:00".freeze + time = source.to_time assert_instance_of Time, time assert_equal @utc_time, time.getutc assert_equal @system_offset, time.utc_offset + assert_not_predicate time, :frozen? end end end diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 0655197335..50bb1004f7 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -4,19 +4,19 @@ require "core_ext/date_and_time_behavior" require "time_zone_test_helpers" class DateExtCalculationsTest < ActiveSupport::TestCase - def date_time_init(year,month,day,*args) - Date.new(year,month,day) + def date_time_init(year, month, day, *args) + Date.new(year, month, day) end include DateAndTimeBehavior include TimeZoneTestHelpers def test_yesterday_in_calendar_reform - assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday + assert_equal Date.new(1582, 10, 4), Date.new(1582, 10, 15).yesterday end def test_tomorrow_in_calendar_reform - assert_equal Date.new(1582,10,15), Date.new(1582,10,4).tomorrow + assert_equal Date.new(1582, 10, 15), Date.new(1582, 10, 4).tomorrow end def test_to_s @@ -83,70 +83,70 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_change assert_equal Date.new(2005, 2, 21), Date.new(2005, 2, 11).change(day: 21) assert_equal Date.new(2007, 5, 11), Date.new(2005, 2, 11).change(year: 2007, month: 5) - assert_equal Date.new(2006,2,22), Date.new(2005,2,22).change(year: 2006) - assert_equal Date.new(2005,6,22), Date.new(2005,2,22).change(month: 6) + assert_equal Date.new(2006, 2, 22), Date.new(2005, 2, 22).change(year: 2006) + assert_equal Date.new(2005, 6, 22), Date.new(2005, 2, 22).change(month: 6) end def test_sunday - assert_equal Date.new(2008,3,2), Date.new(2008,3,02).sunday - assert_equal Date.new(2008,3,2), Date.new(2008,2,29).sunday + assert_equal Date.new(2008, 3, 2), Date.new(2008, 3, 02).sunday + assert_equal Date.new(2008, 3, 2), Date.new(2008, 2, 29).sunday 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 - assert_equal Date.new(2008,12,31).to_s, Date.new(2008,2,22).end_of_year.to_s + assert_equal Date.new(2008, 12, 31).to_s, Date.new(2008, 2, 22).end_of_year.to_s end def test_end_of_month - assert_equal Date.new(2005,3,31), Date.new(2005,3,20).end_of_month - assert_equal Date.new(2005,2,28), Date.new(2005,2,20).end_of_month - assert_equal Date.new(2005,4,30), Date.new(2005,4,20).end_of_month + assert_equal Date.new(2005, 3, 31), Date.new(2005, 3, 20).end_of_month + assert_equal Date.new(2005, 2, 28), Date.new(2005, 2, 20).end_of_month + assert_equal Date.new(2005, 4, 30), Date.new(2005, 4, 20).end_of_month end def test_prev_year_in_leap_years - assert_equal Date.new(1999,2,28), Date.new(2000,2,29).prev_year + assert_equal Date.new(1999, 2, 28), Date.new(2000, 2, 29).prev_year end def test_prev_year_in_calendar_reform - assert_equal Date.new(1582,10,4), Date.new(1583,10,14).prev_year + assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, 14).prev_year end def test_last_year - assert_equal Date.new(2004,6,5), Date.new(2005,6,5).last_year + assert_equal Date.new(2004, 6, 5), Date.new(2005, 6, 5).last_year end def test_last_year_in_leap_years - assert_equal Date.new(1999,2,28), Date.new(2000,2,29).last_year + assert_equal Date.new(1999, 2, 28), Date.new(2000, 2, 29).last_year end def test_last_year_in_calendar_reform - assert_equal Date.new(1582,10,4), Date.new(1583,10,14).last_year + assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, 14).last_year end def test_next_year_in_leap_years - assert_equal Date.new(2001,2,28), Date.new(2000,2,29).next_year + assert_equal Date.new(2001, 2, 28), Date.new(2000, 2, 29).next_year end def test_next_year_in_calendar_reform - assert_equal Date.new(1582,10,4), Date.new(1581,10,10).next_year + assert_equal Date.new(1582, 10, 4), Date.new(1581, 10, 10).next_year end def test_advance - assert_equal Date.new(2006,2,28), Date.new(2005,2,28).advance(years: 1) - assert_equal Date.new(2005,6,28), Date.new(2005,2,28).advance(months: 4) - assert_equal Date.new(2005,3,21), Date.new(2005,2,28).advance(weeks: 3) - assert_equal Date.new(2005,3,5), Date.new(2005,2,28).advance(days: 5) - 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(2006, 2, 28), Date.new(2005, 2, 28).advance(years: 1) + assert_equal Date.new(2005, 6, 28), Date.new(2005, 2, 28).advance(months: 4) + assert_equal Date.new(2005, 3, 21), Date.new(2005, 2, 28).advance(weeks: 3) + assert_equal Date.new(2005, 3, 5), Date.new(2005, 2, 28).advance(days: 5) + 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 end def test_advance_does_first_years_and_then_days @@ -160,27 +160,27 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end def test_advance_in_calendar_reform - assert_equal Date.new(1582,10,15), Date.new(1582,10,4).advance(days: 1) - assert_equal Date.new(1582,10,4), Date.new(1582,10,15).advance(days: -1) + assert_equal Date.new(1582, 10, 15), Date.new(1582, 10, 4).advance(days: 1) + assert_equal Date.new(1582, 10, 4), Date.new(1582, 10, 15).advance(days: -1) 5.upto(14) do |day| - assert_equal Date.new(1582,10,4), Date.new(1582,9,day).advance(months: 1) - assert_equal Date.new(1582,10,4), Date.new(1582,11,day).advance(months: -1) - assert_equal Date.new(1582,10,4), Date.new(1581,10,day).advance(years: 1) - assert_equal Date.new(1582,10,4), Date.new(1583,10,day).advance(years: -1) + assert_equal Date.new(1582, 10, 4), Date.new(1582, 9, day).advance(months: 1) + assert_equal Date.new(1582, 10, 4), Date.new(1582, 11, day).advance(months: -1) + assert_equal Date.new(1582, 10, 4), Date.new(1581, 10, day).advance(years: 1) + assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, day).advance(years: -1) end end def test_last_week - assert_equal Date.new(2005,5,9), Date.new(2005,5,17).last_week - assert_equal Date.new(2006,12,25), Date.new(2007,1,7).last_week - assert_equal Date.new(2010,2,12), Date.new(2010,2,19).last_week(:friday) - assert_equal Date.new(2010,2,13), Date.new(2010,2,19).last_week(:saturday) - assert_equal Date.new(2010,2,27), Date.new(2010,3,4).last_week(:saturday) + assert_equal Date.new(2005, 5, 9), Date.new(2005, 5, 17).last_week + assert_equal Date.new(2006, 12, 25), Date.new(2007, 1, 7).last_week + assert_equal Date.new(2010, 2, 12), Date.new(2010, 2, 19).last_week(:friday) + assert_equal Date.new(2010, 2, 13), Date.new(2010, 2, 19).last_week(:saturday) + assert_equal Date.new(2010, 2, 27), Date.new(2010, 3, 4).last_week(:saturday) end def test_next_week_in_calendar_reform - assert_equal Date.new(1582,10,15), Date.new(1582,9,30).next_week(:friday) - assert_equal Date.new(1582,10,18), Date.new(1582,10,4).next_week + assert_equal Date.new(1582, 10, 15), Date.new(1582, 9, 30).next_week(:friday) + assert_equal Date.new(1582, 10, 18), Date.new(1582, 10, 4).next_week end def test_last_month_on_31st @@ -236,97 +236,97 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end def test_since - assert_equal Time.local(2005,2,21,0,0,45), Date.new(2005,2,21).since(45) + assert_equal Time.local(2005, 2, 21, 0, 0, 45), Date.new(2005, 2, 21).since(45) end def test_since_when_zone_is_set zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] with_env_tz "UTC" do with_tz_default zone do - assert_equal zone.local(2005,2,21,0,0,45), Date.new(2005,2,21).since(45) - assert_equal zone, Date.new(2005,2,21).since(45).time_zone + assert_equal zone.local(2005, 2, 21, 0, 0, 45), Date.new(2005, 2, 21).since(45) + assert_equal zone, Date.new(2005, 2, 21).since(45).time_zone end end end def test_ago - assert_equal Time.local(2005,2,20,23,59,15), Date.new(2005,2,21).ago(45) + assert_equal Time.local(2005, 2, 20, 23, 59, 15), Date.new(2005, 2, 21).ago(45) end def test_ago_when_zone_is_set zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] with_env_tz "UTC" do with_tz_default zone do - assert_equal zone.local(2005,2,20,23,59,15), Date.new(2005,2,21).ago(45) - assert_equal zone, Date.new(2005,2,21).ago(45).time_zone + assert_equal zone.local(2005, 2, 20, 23, 59, 15), Date.new(2005, 2, 21).ago(45) + assert_equal zone, Date.new(2005, 2, 21).ago(45).time_zone end end end def test_beginning_of_day - assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day + assert_equal Time.local(2005, 2, 21, 0, 0, 0), Date.new(2005, 2, 21).beginning_of_day end def test_middle_of_day - assert_equal Time.local(2005,2,21,12,0,0), Date.new(2005,2,21).middle_of_day + assert_equal Time.local(2005, 2, 21, 12, 0, 0), Date.new(2005, 2, 21).middle_of_day end def test_beginning_of_day_when_zone_is_set zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] with_env_tz "UTC" do with_tz_default zone do - assert_equal zone.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day - assert_equal zone, Date.new(2005,2,21).beginning_of_day.time_zone + assert_equal zone.local(2005, 2, 21, 0, 0, 0), Date.new(2005, 2, 21).beginning_of_day + assert_equal zone, Date.new(2005, 2, 21).beginning_of_day.time_zone end end end def test_end_of_day - assert_equal Time.local(2005,2,21,23,59,59,Rational(999999999, 1000)), Date.new(2005,2,21).end_of_day + assert_equal Time.local(2005, 2, 21, 23, 59, 59, Rational(999999999, 1000)), Date.new(2005, 2, 21).end_of_day end def test_end_of_day_when_zone_is_set zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] with_env_tz "UTC" do with_tz_default zone do - assert_equal zone.local(2005,2,21,23,59,59,Rational(999999999, 1000)), Date.new(2005,2,21).end_of_day - assert_equal zone, Date.new(2005,2,21).end_of_day.time_zone + assert_equal zone.local(2005, 2, 21, 23, 59, 59, Rational(999999999, 1000)), Date.new(2005, 2, 21).end_of_day + assert_equal zone, Date.new(2005, 2, 21).end_of_day.time_zone end end end def test_all_day - beginning_of_day = Time.local(2011,6,7,0,0,0) - end_of_day = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)) - assert_equal beginning_of_day..end_of_day, Date.new(2011,6,7).all_day + beginning_of_day = Time.local(2011, 6, 7, 0, 0, 0) + end_of_day = Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000)) + assert_equal beginning_of_day..end_of_day, Date.new(2011, 6, 7).all_day end def test_all_day_when_zone_is_set zone = ActiveSupport::TimeZone["Hawaii"] with_env_tz "UTC" do with_tz_default zone do - beginning_of_day = zone.local(2011,6,7,0,0,0) - end_of_day = zone.local(2011,6,7,23,59,59,Rational(999999999, 1000)) - assert_equal beginning_of_day..end_of_day, Date.new(2011,6,7).all_day + beginning_of_day = zone.local(2011, 6, 7, 0, 0, 0) + end_of_day = zone.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000)) + assert_equal beginning_of_day..end_of_day, Date.new(2011, 6, 7).all_day end end end def test_all_week - assert_equal Date.new(2011,6,6)..Date.new(2011,6,12), Date.new(2011,6,7).all_week - assert_equal Date.new(2011,6,5)..Date.new(2011,6,11), Date.new(2011,6,7).all_week(:sunday) + assert_equal Date.new(2011, 6, 6)..Date.new(2011, 6, 12), Date.new(2011, 6, 7).all_week + assert_equal Date.new(2011, 6, 5)..Date.new(2011, 6, 11), Date.new(2011, 6, 7).all_week(:sunday) end def test_all_month - assert_equal Date.new(2011,6,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_month + assert_equal Date.new(2011, 6, 1)..Date.new(2011, 6, 30), Date.new(2011, 6, 7).all_month end def test_all_quarter - assert_equal Date.new(2011,4,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_quarter + assert_equal Date.new(2011, 4, 1)..Date.new(2011, 6, 30), Date.new(2011, 6, 7).all_quarter end def test_all_year - assert_equal Date.new(2011,1,1)..Date.new(2011,12,31), Date.new(2011,6,7).all_year + assert_equal Date.new(2011, 1, 1)..Date.new(2011, 12, 31), Date.new(2011, 6, 7).all_year end def test_xmlschema @@ -353,16 +353,16 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_past Date.stub(:current, Date.new(2000, 1, 1)) do assert_equal true, Date.new(1999, 12, 31).past? - assert_equal false, Date.new(2000,1,1).past? - assert_equal false, Date.new(2000,1,2).past? + assert_equal false, Date.new(2000, 1, 1).past? + assert_equal false, Date.new(2000, 1, 2).past? end end def test_future Date.stub(:current, Date.new(2000, 1, 1)) do assert_equal false, Date.new(1999, 12, 31).future? - assert_equal false, Date.new(2000,1,1).future? - assert_equal true, Date.new(2000,1,2).future? + assert_equal false, Date.new(2000, 1, 1).future? + assert_equal true, Date.new(2000, 1, 2).future? end end @@ -385,7 +385,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_date_advance_should_not_change_passed_options_hash options = { years: 3, months: 11, days: 2 } - Date.new(2005,2,28).advance(options) + Date.new(2005, 2, 28).advance(options) assert_equal({ years: 3, months: 11, days: 2 }, options) end end @@ -395,6 +395,10 @@ class DateExtBehaviorTest < ActiveSupport::TestCase assert Date.new.acts_like_date? end + def test_blank? + assert_not Date.new.blank? + end + def test_freeze_doesnt_clobber_memoized_instance_methods assert_nothing_raised do Date.today.freeze.inspect diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index f2a50f4693..276fa2bfd3 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -4,8 +4,8 @@ require "core_ext/date_and_time_behavior" require "time_zone_test_helpers" class DateTimeExtCalculationsTest < ActiveSupport::TestCase - def date_time_init(year,month,day,hour,minute,second,*args) - DateTime.civil(year,month,day,hour,minute,second) + def date_time_init(year, month, day, hour, minute, second, usec = 0) + DateTime.civil(year, month, day, hour, minute, second + (usec / 1000000)) end include DateAndTimeBehavior @@ -28,6 +28,28 @@ 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 @@ -45,7 +67,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_instance_of Time, DateTime.new(2016, 3, 11, 15, 11, 12, 0).localtime assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).localtime assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).localtime - assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1,24)).localtime + assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1, 24)).localtime end end @@ -54,7 +76,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_instance_of Time, DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).getlocal - assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1,24)).getlocal + assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1, 24)).getlocal end end @@ -90,108 +112,118 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_seconds_since_midnight - assert_equal 1,DateTime.civil(2005,1,1,0,0,1).seconds_since_midnight - assert_equal 60,DateTime.civil(2005,1,1,0,1,0).seconds_since_midnight - assert_equal 3660,DateTime.civil(2005,1,1,1,1,0).seconds_since_midnight - assert_equal 86399,DateTime.civil(2005,1,1,23,59,59).seconds_since_midnight + assert_equal 1, DateTime.civil(2005, 1, 1, 0, 0, 1).seconds_since_midnight + assert_equal 60, DateTime.civil(2005, 1, 1, 0, 1, 0).seconds_since_midnight + assert_equal 3660, DateTime.civil(2005, 1, 1, 1, 1, 0).seconds_since_midnight + assert_equal 86399, DateTime.civil(2005, 1, 1, 23, 59, 59).seconds_since_midnight end def test_seconds_until_end_of_day - assert_equal 0, DateTime.civil(2005,1,1,23,59,59).seconds_until_end_of_day - assert_equal 1, DateTime.civil(2005,1,1,23,59,58).seconds_until_end_of_day - assert_equal 60, DateTime.civil(2005,1,1,23,58,59).seconds_until_end_of_day - assert_equal 3660, DateTime.civil(2005,1,1,22,58,59).seconds_until_end_of_day - assert_equal 86399, DateTime.civil(2005,1,1,0,0,0).seconds_until_end_of_day + assert_equal 0, DateTime.civil(2005, 1, 1, 23, 59, 59).seconds_until_end_of_day + assert_equal 1, DateTime.civil(2005, 1, 1, 23, 59, 58).seconds_until_end_of_day + assert_equal 60, DateTime.civil(2005, 1, 1, 23, 58, 59).seconds_until_end_of_day + assert_equal 3660, DateTime.civil(2005, 1, 1, 22, 58, 59).seconds_until_end_of_day + assert_equal 86399, DateTime.civil(2005, 1, 1, 0, 0, 0).seconds_until_end_of_day end def test_beginning_of_day - assert_equal DateTime.civil(2005,2,4,0,0,0), DateTime.civil(2005,2,4,10,10,10).beginning_of_day + assert_equal DateTime.civil(2005, 2, 4, 0, 0, 0), DateTime.civil(2005, 2, 4, 10, 10, 10).beginning_of_day end def test_middle_of_day - assert_equal DateTime.civil(2005,2,4,12,0,0), DateTime.civil(2005,2,4,10,10,10).middle_of_day + assert_equal DateTime.civil(2005, 2, 4, 12, 0, 0), DateTime.civil(2005, 2, 4, 10, 10, 10).middle_of_day end def test_end_of_day - assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day + assert_equal DateTime.civil(2005, 2, 4, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 10, 10, 10).end_of_day end def test_beginning_of_hour - assert_equal DateTime.civil(2005,2,4,19,0,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_hour + assert_equal DateTime.civil(2005, 2, 4, 19, 0, 0), DateTime.civil(2005, 2, 4, 19, 30, 10).beginning_of_hour end def test_end_of_hour - assert_equal DateTime.civil(2005,2,4,19,59,59), DateTime.civil(2005,2,4,19,30,10).end_of_hour + assert_equal DateTime.civil(2005, 2, 4, 19, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_hour end def test_beginning_of_minute - assert_equal DateTime.civil(2005,2,4,19,30,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_minute + assert_equal DateTime.civil(2005, 2, 4, 19, 30, 0), DateTime.civil(2005, 2, 4, 19, 30, 10).beginning_of_minute end def test_end_of_minute - assert_equal DateTime.civil(2005,2,4,19,30,59), DateTime.civil(2005,2,4,19,30,10).end_of_minute + assert_equal DateTime.civil(2005, 2, 4, 19, 30, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_minute end def test_end_of_month - assert_equal DateTime.civil(2005,3,31,23,59,59), DateTime.civil(2005,3,20,10,10,10).end_of_month - assert_equal DateTime.civil(2005,2,28,23,59,59), DateTime.civil(2005,2,20,10,10,10).end_of_month - assert_equal DateTime.civil(2005,4,30,23,59,59), DateTime.civil(2005,4,20,10,10,10).end_of_month + assert_equal DateTime.civil(2005, 3, 31, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 3, 20, 10, 10, 10).end_of_month + assert_equal DateTime.civil(2005, 2, 28, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 20, 10, 10, 10).end_of_month + assert_equal DateTime.civil(2005, 4, 30, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 4, 20, 10, 10, 10).end_of_month end def test_last_year - assert_equal DateTime.civil(2004,6,5,10), DateTime.civil(2005,6,5,10,0,0).last_year + assert_equal DateTime.civil(2004, 6, 5, 10), DateTime.civil(2005, 6, 5, 10, 0, 0).last_year end def test_ago - assert_equal DateTime.civil(2005,2,22,10,10,9), DateTime.civil(2005,2,22,10,10,10).ago(1) - assert_equal DateTime.civil(2005,2,22,9,10,10), DateTime.civil(2005,2,22,10,10,10).ago(3600) - assert_equal DateTime.civil(2005,2,20,10,10,10), DateTime.civil(2005,2,22,10,10,10).ago(86400*2) - assert_equal DateTime.civil(2005,2,20,9,9,45), DateTime.civil(2005,2,22,10,10,10).ago(86400*2 + 3600 + 25) + assert_equal DateTime.civil(2005, 2, 22, 10, 10, 9), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(1) + assert_equal DateTime.civil(2005, 2, 22, 9, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(3600) + assert_equal DateTime.civil(2005, 2, 20, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(86400 * 2) + assert_equal DateTime.civil(2005, 2, 20, 9, 9, 45), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(86400 * 2 + 3600 + 25) end def test_since - assert_equal DateTime.civil(2005,2,22,10,10,11), DateTime.civil(2005,2,22,10,10,10).since(1) - assert_equal DateTime.civil(2005,2,22,11,10,10), DateTime.civil(2005,2,22,10,10,10).since(3600) - assert_equal DateTime.civil(2005,2,24,10,10,10), DateTime.civil(2005,2,22,10,10,10).since(86400*2) - assert_equal DateTime.civil(2005,2,24,11,10,35), DateTime.civil(2005,2,22,10,10,10).since(86400*2 + 3600 + 25) - assert_equal DateTime.civil(2005,2,22,10,10,11), DateTime.civil(2005,2,22,10,10,10).since(1.333) - assert_equal DateTime.civil(2005,2,22,10,10,12), DateTime.civil(2005,2,22,10,10,10).since(1.667) + assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1) + assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600) + assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2) + assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25) + assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) + assert_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) end def test_change - assert_equal DateTime.civil(2006,2,22,15,15,10), DateTime.civil(2005,2,22,15,15,10).change(year: 2006) - assert_equal DateTime.civil(2005,6,22,15,15,10), DateTime.civil(2005,2,22,15,15,10).change(month: 6) - assert_equal DateTime.civil(2012,9,22,15,15,10), DateTime.civil(2005,2,22,15,15,10).change(year: 2012, month: 9) - assert_equal DateTime.civil(2005,2,22,16), DateTime.civil(2005,2,22,15,15,10).change(hour: 16) - assert_equal DateTime.civil(2005,2,22,16,45), DateTime.civil(2005,2,22,15,15,10).change(hour: 16, min: 45) - assert_equal DateTime.civil(2005,2,22,15,45), DateTime.civil(2005,2,22,15,15,10).change(min: 45) + assert_equal DateTime.civil(2006, 2, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(year: 2006) + assert_equal DateTime.civil(2005, 6, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(month: 6) + assert_equal DateTime.civil(2012, 9, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9) + assert_equal DateTime.civil(2005, 2, 22, 16), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16) + assert_equal DateTime.civil(2005, 2, 22, 16, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45) + assert_equal DateTime.civil(2005, 2, 22, 15, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(min: 45) + + # datetime with non-zero offset + assert_equal DateTime.civil(2005, 2, 22, 15, 15, 10, Rational(-5, 24)), DateTime.civil(2005, 2, 22, 15, 15, 10, 0).change(offset: Rational(-5, 24)) # datetime with fractions of a second - assert_equal DateTime.civil(2005,2,1,15,15,10.7), DateTime.civil(2005,2,22,15,15,10.7).change(day: 1) + assert_equal DateTime.civil(2005, 2, 1, 15, 15, 10.7), DateTime.civil(2005, 2, 22, 15, 15, 10.7).change(day: 1) + assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(usec: 8) + assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(nsec: 8000) + assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 1, nsec: 1) } + assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 1000000) } + assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(nsec: 1000000000) } + assert_nothing_raised { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 999999) } + assert_nothing_raised { DateTime.civil(2005, 1, 2, 11, 22, 0).change(nsec: 999999999) } end def test_advance - assert_equal DateTime.civil(2006,2,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(years: 1) - assert_equal DateTime.civil(2005,6,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(months: 4) - assert_equal DateTime.civil(2005,3,21,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(weeks: 3) - assert_equal DateTime.civil(2005,3,5,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(days: 5) - assert_equal DateTime.civil(2012,9,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(years: 7, months: 7) - 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,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) - assert_equal DateTime.civil(2005,2,28,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(hours: 5, minutes: 7, seconds: 9) - assert_equal DateTime.civil(2005,2,28,10,8,1), DateTime.civil(2005,2,28,15,15,10).advance(hours: -5, minutes: -7, seconds: -9) - assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) + assert_equal DateTime.civil(2006, 2, 28, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 1) + assert_equal DateTime.civil(2005, 6, 28, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(months: 4) + assert_equal DateTime.civil(2005, 3, 21, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(weeks: 3) + assert_equal DateTime.civil(2005, 3, 5, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(days: 5) + assert_equal DateTime.civil(2012, 9, 28, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 7) + 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, 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) + assert_equal DateTime.civil(2005, 2, 28, 20, 22, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: 5, minutes: 7, seconds: 9) + assert_equal DateTime.civil(2005, 2, 28, 10, 8, 1), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: -5, minutes: -7, seconds: -9) + assert_equal DateTime.civil(2013, 10, 17, 20, 22, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) end def test_advance_partial_days - assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(days: 1.5) - assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(days: 0.5) - assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(days: 1.5, months: 1) + assert_equal DateTime.civil(2012, 9, 29, 13, 15, 10), DateTime.civil(2012, 9, 28, 1, 15, 10).advance(days: 1.5) + assert_equal DateTime.civil(2012, 9, 28, 13, 15, 10), DateTime.civil(2012, 9, 28, 1, 15, 10).advance(days: 0.5) + assert_equal DateTime.civil(2012, 10, 29, 13, 15, 10), DateTime.civil(2012, 9, 28, 1, 15, 10).advance(days: 1.5, months: 1) end def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas @@ -203,11 +235,11 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_last_week - assert_equal DateTime.civil(2005,2,21), DateTime.civil(2005,3,1,15,15,10).last_week - assert_equal DateTime.civil(2005,2,22), DateTime.civil(2005,3,1,15,15,10).last_week(:tuesday) - assert_equal DateTime.civil(2005,2,25), DateTime.civil(2005,3,1,15,15,10).last_week(:friday) - assert_equal DateTime.civil(2006,10,30), DateTime.civil(2006,11,6,0,0,0).last_week - assert_equal DateTime.civil(2006,11,15), DateTime.civil(2006,11,23,0,0,0).last_week(:wednesday) + assert_equal DateTime.civil(2005, 2, 21), DateTime.civil(2005, 3, 1, 15, 15, 10).last_week + assert_equal DateTime.civil(2005, 2, 22), DateTime.civil(2005, 3, 1, 15, 15, 10).last_week(:tuesday) + assert_equal DateTime.civil(2005, 2, 25), DateTime.civil(2005, 3, 1, 15, 15, 10).last_week(:friday) + assert_equal DateTime.civil(2006, 10, 30), DateTime.civil(2006, 11, 6, 0, 0, 0).last_week + assert_equal DateTime.civil(2006, 11, 15), DateTime.civil(2006, 11, 23, 0, 0, 0).last_week(:wednesday) end def test_date_time_should_have_correct_last_week_for_leap_year @@ -233,51 +265,51 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase def test_today_with_offset Date.stub(:current, Date.new(2000, 1, 1)) do - assert_equal false, DateTime.civil(1999,12,31,23,59,59, Rational(-18000, 86400)).today? - assert_equal true, DateTime.civil(2000,1,1,0,0,0, Rational(-18000, 86400)).today? - assert_equal true, DateTime.civil(2000,1,1,23,59,59, Rational(-18000, 86400)).today? - assert_equal false, DateTime.civil(2000,1,2,0,0,0, Rational(-18000, 86400)).today? + assert_equal false, DateTime.civil(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)).today? + assert_equal true, DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-18000, 86400)).today? + assert_equal true, DateTime.civil(2000, 1, 1, 23, 59, 59, Rational(-18000, 86400)).today? + assert_equal false, DateTime.civil(2000, 1, 2, 0, 0, 0, Rational(-18000, 86400)).today? end end def test_today_without_offset Date.stub(:current, Date.new(2000, 1, 1)) do - assert_equal false, DateTime.civil(1999,12,31,23,59,59).today? - assert_equal true, DateTime.civil(2000,1,1,0).today? - assert_equal true, DateTime.civil(2000,1,1,23,59,59).today? - assert_equal false, DateTime.civil(2000,1,2,0).today? + assert_equal false, DateTime.civil(1999, 12, 31, 23, 59, 59).today? + assert_equal true, DateTime.civil(2000, 1, 1, 0).today? + assert_equal true, DateTime.civil(2000, 1, 1, 23, 59, 59).today? + assert_equal false, DateTime.civil(2000, 1, 2, 0).today? end end def test_past_with_offset - DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do - assert_equal true, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).past? - assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).past? - assert_equal false, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).past? + DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do + assert_equal true, DateTime.civil(2005, 2, 10, 15, 30, 44, Rational(-18000, 86400)).past? + assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400)).past? + assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 46, Rational(-18000, 86400)).past? end end def test_past_without_offset - DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do - assert_equal true, DateTime.civil(2005,2,10,20,30,44).past? - assert_equal false, DateTime.civil(2005,2,10,20,30,45).past? - assert_equal false, DateTime.civil(2005,2,10,20,30,46).past? + DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do + assert_equal true, DateTime.civil(2005, 2, 10, 20, 30, 44).past? + assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 45).past? + assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 46).past? end end def test_future_with_offset - DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do - assert_equal false, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).future? - assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).future? - assert_equal true, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).future? + DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do + assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 44, Rational(-18000, 86400)).future? + assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400)).future? + assert_equal true, DateTime.civil(2005, 2, 10, 15, 30, 46, Rational(-18000, 86400)).future? end end def test_future_without_offset - DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do - assert_equal false, DateTime.civil(2005,2,10,20,30,44).future? - assert_equal false, DateTime.civil(2005,2,10,20,30,45).future? - assert_equal true, DateTime.civil(2005,2,10,20,30,46).future? + DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do + assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 44).future? + assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 45).future? + assert_equal true, DateTime.civil(2005, 2, 10, 20, 30, 46).future? end end @@ -318,6 +350,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert DateTime.new.acts_like_time? end + def test_blank? + assert_not DateTime.new.blank? + end + def test_utc? assert_equal true, DateTime.civil(2005, 2, 21, 10, 11, 12).utc? assert_equal true, DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc? @@ -329,8 +365,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal 0, DateTime.civil(2005, 2, 21, 10, 11, 12).utc_offset assert_equal 0, DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc_offset assert_equal 21600, DateTime.civil(2005, 2, 21, 10, 11, 12, 0.25).utc_offset - assert_equal( -21600, DateTime.civil(2005, 2, 21, 10, 11, 12, -0.25).utc_offset ) - assert_equal( -18000, DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).utc_offset ) + assert_equal(-21600, DateTime.civil(2005, 2, 21, 10, 11, 12, -0.25).utc_offset) + assert_equal(-18000, DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).utc_offset) end def test_utc @@ -367,16 +403,16 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase 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(2000, 1, 1, 0, 0, 1), 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(2000, 1, 1, 0, 0, 1).to_s) - assert_equal nil, DateTime.civil(2000) <=> "Invalid as Time" + 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 @@ -399,27 +435,27 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase def test_to_f assert_equal 946684800.0, DateTime.civil(2000).to_f - assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f - assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f + assert_equal 946684800.0, DateTime.civil(1999, 12, 31, 19, 0, 0, Rational(-5, 24)).to_f + assert_equal 946684800.5, DateTime.civil(1999, 12, 31, 19, 0, 0.5, Rational(-5, 24)).to_f end def test_to_i assert_equal 946684800, DateTime.civil(2000).to_i - assert_equal 946684800, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_i + assert_equal 946684800, DateTime.civil(1999, 12, 31, 19, 0, 0, Rational(-5, 24)).to_i end def test_usec assert_equal 0, DateTime.civil(2000).usec - assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).usec + assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)).usec end def test_nsec assert_equal 0, DateTime.civil(2000).nsec - assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec + assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)).nsec end def test_subsec assert_equal 0, DateTime.civil(2000).subsec - assert_equal Rational(1,2), DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).subsec + assert_equal Rational(1, 2), DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)).subsec end end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index a881768470..3108f24f21 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -21,7 +21,7 @@ class DurationTest < ActiveSupport::TestCase end def test_instance_of - assert 1.minute.instance_of?(Fixnum) + assert 1.minute.instance_of?(1.class) assert 2.days.instance_of?(ActiveSupport::Duration) assert !3.second.instance_of?(Numeric) end @@ -75,7 +75,7 @@ class DurationTest < ActiveSupport::TestCase current_locale = I18n.default_locale I18n.default_locale = :de I18n.backend.store_translations(:de, support: { array: { last_word_connector: " und " } }) - assert_equal "10 years, 1 month und 1 day", (10.years + 1.month + 1.day).inspect + assert_equal "10 years, 1 month und 1 day", (10.years + 1.month + 1.day).inspect ensure I18n.default_locale = current_locale end @@ -84,12 +84,44 @@ class DurationTest < ActiveSupport::TestCase assert_nothing_raised { Date.today - Date.today } end + def test_plus + assert_equal 2.seconds, 1.second + 1.second + assert_instance_of ActiveSupport::Duration, 1.second + 1.second + assert_equal 2.seconds, 1.second + 1 + assert_instance_of ActiveSupport::Duration, 1.second + 1 + end + + def test_minus + assert_equal 1.second, 2.seconds - 1.second + assert_instance_of ActiveSupport::Duration, 2.seconds - 1.second + assert_equal 1.second, 2.seconds - 1 + assert_instance_of ActiveSupport::Duration, 2.seconds - 1 + assert_equal 1.second, 2 - 1.second + assert_instance_of ActiveSupport::Duration, 2.seconds - 1 + end + + def test_multiply + assert_equal 7.days, 1.day * 7 + assert_instance_of ActiveSupport::Duration, 1.day * 7 + assert_equal 86400, 1.day * 1.second + end + + def test_divide + assert_equal 1.day, 7.days / 7 + assert_instance_of ActiveSupport::Duration, 7.days / 7 + assert_equal 1, 1.day / 1.day + end + + def test_date_added_with_multiplied_duration + assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 1.day * 2 + end + def test_plus_with_time assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration" 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") @@ -153,10 +185,10 @@ class DurationTest < ActiveSupport::TestCase Time.stub(:now, Time.local(2000)) do # since assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since - assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since + assert_equal Time.local(2000, 1, 1, 0, 0, 5), 5.seconds.since # ago assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago - assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago + assert_equal Time.local(1999, 12, 31, 23, 59, 55), 5.seconds.ago end end end @@ -167,11 +199,11 @@ class DurationTest < ActiveSupport::TestCase Time.stub(:now, Time.local(2000)) do # since assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since - assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time + assert_equal Time.utc(2000, 1, 1, 0, 0, 5), 5.seconds.since.time assert_equal "Eastern Time (US & Canada)", 5.seconds.since.time_zone.name # ago assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago - assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time + assert_equal Time.utc(1999, 12, 31, 23, 59, 55), 5.seconds.ago.time assert_equal "Eastern Time (US & Canada)", 5.seconds.ago.time_zone.name end end @@ -179,15 +211,28 @@ class DurationTest < ActiveSupport::TestCase Time.zone = nil end + def test_before_and_afer + t = Time.local(2000) + assert_equal t + 1, 1.second.after(t) + assert_equal t - 1, 1.second.before(t) + end + + def test_before_and_after_without_argument + Time.stub(:now, Time.local(2000)) do + assert_equal Time.now - 1.second, 1.second.before + assert_equal Time.now + 1.second, 1.second.after + end + end + def test_adding_hours_across_dst_boundary with_env_tz "CET" do - assert_equal Time.local(2009,3,29,0,0,0) + 24.hours, Time.local(2009,3,30,1,0,0) + assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 24.hours, Time.local(2009, 3, 30, 1, 0, 0) end end def test_adding_day_across_dst_boundary with_env_tz "CET" do - assert_equal Time.local(2009,3,29,0,0,0) + 1.day, Time.local(2009,3,30,0,0,0) + assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 1.day, Time.local(2009, 3, 30, 0, 0, 0) end end @@ -196,7 +241,7 @@ class DurationTest < ActiveSupport::TestCase assert_nothing_raised do 1.minute.times { counter += 1 } end - assert_equal counter, 60 + assert_equal 60, counter end def test_as_json @@ -213,7 +258,7 @@ class DurationTest < ActiveSupport::TestCase when 1.day "ok" end - assert_equal cased, "ok" + assert_equal "ok", cased end def test_respond_to @@ -237,6 +282,175 @@ class DurationTest < ActiveSupport::TestCase assert_equal(1, (61 <=> 1.minute)) end + def test_implicit_coercion + assert_equal 2.days, 2 * 1.day + assert_instance_of ActiveSupport::Duration, 2 * 1.day + assert_equal Time.utc(2017, 1, 3), Time.utc(2017, 1, 1) + 2 * 1.day + assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 2 * 1.day + end + + def test_scalar_coerce + scalar = ActiveSupport::Duration::Scalar.new(10) + assert_instance_of ActiveSupport::Duration::Scalar, 10 + scalar + assert_instance_of ActiveSupport::Duration, 10.seconds + scalar + end + + def test_scalar_delegations + scalar = ActiveSupport::Duration::Scalar.new(10) + assert_kind_of Float, scalar.to_f + assert_kind_of Integer, scalar.to_i + assert_kind_of String, scalar.to_s + end + + def test_scalar_unary_minus + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal(-10, -scalar) + assert_instance_of ActiveSupport::Duration::Scalar, -scalar + end + + def test_scalar_compare + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal(1, scalar <=> 5) + assert_equal(0, scalar <=> 10) + assert_equal(-1, scalar <=> 15) + assert_equal(nil, scalar <=> "foo") + end + + def test_scalar_plus + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal 20, 10 + scalar + assert_instance_of ActiveSupport::Duration::Scalar, 10 + scalar + assert_equal 20, scalar + 10 + assert_instance_of ActiveSupport::Duration::Scalar, scalar + 10 + assert_equal 20, 10.seconds + scalar + assert_instance_of ActiveSupport::Duration, 10.seconds + scalar + assert_equal 20, scalar + 10.seconds + assert_instance_of ActiveSupport::Duration, scalar + 10.seconds + + exception = assert_raises(TypeError) do + scalar + "foo" + end + + assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message + end + + def test_scalar_plus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts) + assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts) + end + + def test_scalar_minus + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal 10, 20 - scalar + assert_instance_of ActiveSupport::Duration::Scalar, 20 - scalar + assert_equal 5, scalar - 5 + assert_instance_of ActiveSupport::Duration::Scalar, scalar - 5 + assert_equal 10, 20.seconds - scalar + assert_instance_of ActiveSupport::Duration, 20.seconds - scalar + assert_equal 5, scalar - 5.seconds + assert_instance_of ActiveSupport::Duration, scalar - 5.seconds + + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + + exception = assert_raises(TypeError) do + scalar - "foo" + end + + assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message + end + + def test_scalar_minus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + end + + def test_scalar_multiply + scalar = ActiveSupport::Duration::Scalar.new(5) + + assert_equal 10, 2 * scalar + assert_instance_of ActiveSupport::Duration::Scalar, 2 * scalar + assert_equal 10, scalar * 2 + assert_instance_of ActiveSupport::Duration::Scalar, scalar * 2 + assert_equal 10, 2.seconds * scalar + assert_instance_of ActiveSupport::Duration, 2.seconds * scalar + assert_equal 10, scalar * 2.seconds + assert_instance_of ActiveSupport::Duration, scalar * 2.seconds + + exception = assert_raises(TypeError) do + scalar * "foo" + end + + assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message + end + + def test_scalar_multiply_parts + scalar = ActiveSupport::Duration::Scalar.new(1) + assert_equal({ days: 2 }, (scalar * 2.days).parts) + assert_equal(172800, (scalar * 2.days).value) + assert_equal({ days: -2 }, (scalar * -2.days).parts) + assert_equal(-172800, (scalar * -2.days).value) + end + + def test_scalar_divide + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal 10, 100 / scalar + assert_instance_of ActiveSupport::Duration::Scalar, 100 / scalar + assert_equal 5, scalar / 2 + assert_instance_of ActiveSupport::Duration::Scalar, scalar / 2 + assert_equal 10, 100.seconds / scalar + assert_instance_of ActiveSupport::Duration, 2.seconds * scalar + assert_equal 5, scalar / 2.seconds + assert_instance_of ActiveSupport::Duration, scalar / 2.seconds + + exception = assert_raises(TypeError) do + scalar / "foo" + end + + assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message + end + + def test_scalar_divide_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 2 }, (scalar / 5.days).parts) + assert_equal(172800, (scalar / 5.days).value) + assert_equal({ days: -2 }, (scalar / -5.days).parts) + assert_equal(-172800, (scalar / -5.days).value) + end + + def test_twelve_months_equals_one_year + assert_equal 12.months, 1.year + end + + def test_thirty_days_does_not_equal_one_month + assert_not_equal 30.days, 1.month + end + + def test_adding_one_month_maintains_day_of_month + (1..11).each do |month| + [1, 14, 28].each do |day| + assert_equal Date.civil(2016, month + 1, day), Date.civil(2016, month, day) + 1.month + end + end + + assert_equal Date.civil(2017, 1, 1), Date.civil(2016, 12, 1) + 1.month + assert_equal Date.civil(2017, 1, 14), Date.civil(2016, 12, 14) + 1.month + assert_equal Date.civil(2017, 1, 28), Date.civil(2016, 12, 28) + 1.month + + assert_equal Date.civil(2015, 2, 28), Date.civil(2015, 1, 31) + 1.month + assert_equal Date.civil(2016, 2, 29), Date.civil(2016, 1, 31) + 1.month + end + # ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb # published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE # @@ -292,11 +506,11 @@ class DurationTest < ActiveSupport::TestCase def test_iso8601_output_precision expectations = [ - [nil, "P1Y1MT5.55S", 1.year + 1.month + (5.55).seconds ], - [0, "P1Y1MT6S", 1.year + 1.month + (5.55).seconds ], - [1, "P1Y1MT5.5S", 1.year + 1.month + (5.55).seconds ], - [2, "P1Y1MT5.55S", 1.year + 1.month + (5.55).seconds ], - [3, "P1Y1MT5.550S", 1.year + 1.month + (5.55).seconds ], + [nil, "P1Y1MT8.55S", 1.year + 1.month + (8.55).seconds ], + [0, "P1Y1MT9S", 1.year + 1.month + (8.55).seconds ], + [1, "P1Y1MT8.6S", 1.year + 1.month + (8.55).seconds ], + [2, "P1Y1MT8.55S", 1.year + 1.month + (8.55).seconds ], + [3, "P1Y1MT8.550S", 1.year + 1.month + (8.55).seconds ], [nil, "PT1S", 1.second ], [2, "PT1.00S", 1.second ], [nil, "PT1.4S", (1.4).seconds ], @@ -319,7 +533,64 @@ class DurationTest < ActiveSupport::TestCase time = Time.current patterns.each do |pattern| duration = ActiveSupport::Duration.parse(pattern) - assert_equal time+duration, time+ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect + assert_equal time + duration, time + ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect + end + end + + def test_iso8601_parsing_across_spring_dst_boundary + with_env_tz eastern_time_zone do + with_tz_default "Eastern Time (US & Canada)" do + travel_to Time.utc(2016, 3, 11) do + assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i + assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i + end + end end end + + def test_iso8601_parsing_across_autumn_dst_boundary + with_env_tz eastern_time_zone do + with_tz_default "Eastern Time (US & Canada)" do + travel_to Time.utc(2016, 11, 4) do + assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i + assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i + end + end + end + end + + def test_iso8601_parsing_equivalence_with_numeric_extensions_over_long_periods + with_env_tz eastern_time_zone do + with_tz_default "Eastern Time (US & Canada)" do + assert_equal 3.months, ActiveSupport::Duration.parse("P3M") + assert_equal 3.months.to_i, ActiveSupport::Duration.parse("P3M").to_i + assert_equal 10.months, ActiveSupport::Duration.parse("P10M") + assert_equal 10.months.to_i, ActiveSupport::Duration.parse("P10M").to_i + assert_equal 3.years, ActiveSupport::Duration.parse("P3Y") + assert_equal 3.years.to_i, ActiveSupport::Duration.parse("P3Y").to_i + assert_equal 10.years, ActiveSupport::Duration.parse("P10Y") + assert_equal 10.years.to_i, ActiveSupport::Duration.parse("P10Y").to_i + end + end + end + + def test_adding_durations_do_not_hold_prior_states + time = Time.parse("Nov 29, 2016") + # If the implementation adds and subtracts 3 months, the + # resulting date would have been in February so the day will + # change to the 29th. + d1 = 3.months - 3.months + d2 = 2.months - 2.months + + assert_equal time + d1, time + d2 + end + + private + def eastern_time_zone + if Gem.win_platform? + "EST5EDT" + else + "America/New_York" + end + end end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 9072957e0e..0b345ecf0f 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -22,7 +22,7 @@ class EnumerableTests < ActiveSupport::TestCase end end - def assert_typed_equal(e, v, cls, msg=nil) + def assert_typed_equal(e, v, cls, msg = nil) assert_kind_of(cls, v, msg) assert_equal(e, v, msg) end @@ -70,7 +70,7 @@ class EnumerableTests < ActiveSupport::TestCase sum = GenericEnumerable.new([1.quo(2), 1.quo(3)]).sum assert_typed_equal(5.quo(6), sum, Rational) - sum = GenericEnumerable.new([2.0, 3.0*Complex::I]).sum + sum = GenericEnumerable.new([2.0, 3.0 * Complex::I]).sum assert_typed_equal(Complex(2.0, 3.0), sum, Complex) assert_typed_equal(2.0, sum.real, Float) assert_typed_equal(3.0, sum.imag, Float) @@ -157,7 +157,7 @@ class EnumerableTests < ActiveSupport::TestCase sum = [1.quo(2), 1.quo(3)].sum assert_typed_equal(5.quo(6), sum, Rational) - sum = [2.0, 3.0*Complex::I].sum + sum = [2.0, 3.0 * Complex::I].sum assert_typed_equal(Complex(2.0, 3.0), sum, Complex) assert_typed_equal(2.0, sum.real, Float) assert_typed_equal(3.0, sum.imag, Float) @@ -171,27 +171,25 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by(&:price)) assert_equal Enumerator, payments.index_by.class - if Enumerator.method_defined? :size - assert_equal nil, payments.index_by.size - assert_equal 42, (1..42).index_by.size - end + assert_nil payments.index_by.size + assert_equal 42, (1..42).index_by.size assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by.each(&:price)) end def test_many - assert_equal false, GenericEnumerable.new([] ).many? - assert_equal false, GenericEnumerable.new([ 1 ] ).many? - assert_equal true, GenericEnumerable.new([ 1, 2 ] ).many? + assert_equal false, GenericEnumerable.new([]).many? + assert_equal false, GenericEnumerable.new([ 1 ]).many? + assert_equal true, GenericEnumerable.new([ 1, 2 ]).many? - assert_equal false, GenericEnumerable.new([] ).many? { |x| x > 1 } - assert_equal false, GenericEnumerable.new([ 2 ] ).many? { |x| x > 1 } - assert_equal false, GenericEnumerable.new([ 1, 2 ] ).many? { |x| x > 1 } + assert_equal false, GenericEnumerable.new([]).many? { |x| x > 1 } + assert_equal false, GenericEnumerable.new([ 2 ]).many? { |x| x > 1 } + assert_equal false, GenericEnumerable.new([ 1, 2 ]).many? { |x| x > 1 } assert_equal true, GenericEnumerable.new([ 1, 2, 2 ]).many? { |x| x > 1 } end def test_many_iterates_only_on_what_is_needed - infinity = 1.0/0.0 + infinity = 1.0 / 0.0 very_long_enum = 0..infinity assert_equal true, very_long_enum.many? assert_equal true, very_long_enum.many? { |x| x > 100 } diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index ae35adb19b..18da5fcf5f 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -8,31 +8,6 @@ require "active_support/core_ext/object/deep_dup" require "active_support/inflections" class HashExtTest < ActiveSupport::TestCase - class IndifferentHash < ActiveSupport::HashWithIndifferentAccess - end - - class SubclassingArray < Array - end - - class SubclassingHash < Hash - end - - class NonIndifferentHash < Hash - def nested_under_indifferent_access - self - end - end - - class HashByConversion - def initialize(hash) - @hash = hash - end - - def to_hash - @hash - end - end - def setup @strings = { "a" => 1, "b" => 2 } @nested_strings = { "a" => { "b" => { "c" => 3 } } } @@ -111,7 +86,7 @@ class HashExtTest < ActiveSupport::TestCase transformed_hash = @mixed.dup transformed_hash.transform_keys! { |key| key.to_s.upcase } assert_equal @upcase_strings, transformed_hash - assert_equal @mixed, :a => 1, "b" => 2 + assert_equal({ :a => 1, "b" => 2 }, @mixed) end def test_deep_transform_keys! @@ -127,7 +102,7 @@ class HashExtTest < ActiveSupport::TestCase transformed_hash = @nested_mixed.deep_dup transformed_hash.deep_transform_keys! { |key| key.to_s.upcase } assert_equal @nested_upcase_strings, transformed_hash - assert_equal @nested_mixed, "a" => { b: { "c" => 3 } } + assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed) end def test_symbolize_keys @@ -167,7 +142,7 @@ class HashExtTest < ActiveSupport::TestCase transformed_hash = @mixed.dup transformed_hash.deep_symbolize_keys! assert_equal @symbols, transformed_hash - assert_equal @mixed, :a => 1, "b" => 2 + assert_equal({ :a => 1, "b" => 2 }, @mixed) end def test_deep_symbolize_keys! @@ -183,7 +158,7 @@ class HashExtTest < ActiveSupport::TestCase transformed_hash = @nested_mixed.deep_dup transformed_hash.deep_symbolize_keys! assert_equal @nested_symbols, transformed_hash - assert_equal @nested_mixed, "a" => { b: { "c" => 3 } } + assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed) end def test_symbolize_keys_preserves_keys_that_cant_be_symbolized @@ -243,7 +218,7 @@ class HashExtTest < ActiveSupport::TestCase transformed_hash = @mixed.dup transformed_hash.stringify_keys! assert_equal @strings, transformed_hash - assert_equal @mixed, :a => 1, "b" => 2 + assert_equal({ :a => 1, "b" => 2 }, @mixed) end def test_deep_stringify_keys! @@ -259,452 +234,7 @@ class HashExtTest < ActiveSupport::TestCase transformed_hash = @nested_mixed.deep_dup transformed_hash.deep_stringify_keys! assert_equal @nested_strings, transformed_hash - assert_equal @nested_mixed, "a" => { b: { "c" => 3 } } - end - - def test_symbolize_keys_for_hash_with_indifferent_access - assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys - assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys - assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys - assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys - end - - def test_deep_symbolize_keys_for_hash_with_indifferent_access - assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys - assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys - assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys - assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys - end - - def test_symbolize_keys_bang_for_hash_with_indifferent_access - assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! } - assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! } - assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! } - end - - def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access - assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } - assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! } - assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! } - end - - def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access - assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys - assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! } - end - - def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access - assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys - assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } - end - - def test_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access - assert_equal @integers, @integers.with_indifferent_access.symbolize_keys - assert_raise(NoMethodError) { @integers.with_indifferent_access.dup.symbolize_keys! } - end - - def test_deep_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access - assert_equal @nested_integers, @nested_integers.with_indifferent_access.deep_symbolize_keys - assert_raise(NoMethodError) { @nested_integers.with_indifferent_access.deep_dup.deep_symbolize_keys! } - end - - def test_stringify_keys_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys - assert_equal @strings, @symbols.with_indifferent_access.stringify_keys - assert_equal @strings, @strings.with_indifferent_access.stringify_keys - assert_equal @strings, @mixed.with_indifferent_access.stringify_keys - end - - def test_deep_stringify_keys_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys - assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys - assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys - assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys - end - - def test_stringify_keys_bang_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys! - assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys! - assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys! - assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! - end - - def test_deep_stringify_keys_bang_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys! - assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys! - assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys! - assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys! - end - - def test_nested_under_indifferent_access - foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access - assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"] - - foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access - assert_kind_of NonIndifferentHash, foo["foo"] - - foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access - assert_kind_of IndifferentHash, foo["foo"] - end - - def test_indifferent_assorted - @strings = @strings.with_indifferent_access - @symbols = @symbols.with_indifferent_access - @mixed = @mixed.with_indifferent_access - - assert_equal "a", @strings.__send__(:convert_key, :a) - - assert_equal 1, @strings.fetch("a") - assert_equal 1, @strings.fetch(:a.to_s) - assert_equal 1, @strings.fetch(:a) - - hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed } - method_map = { '[]': 1, fetch: 1, values_at: [1], - has_key?: true, include?: true, key?: true, - member?: true } - - hashes.each do |name, hash| - method_map.sort_by(&:to_s).each do |meth, expected| - assert_equal(expected, hash.__send__(meth, "a"), - "Calling #{name}.#{meth} 'a'") - assert_equal(expected, hash.__send__(meth, :a), - "Calling #{name}.#{meth} :a") - end - end - - assert_equal [1, 2], @strings.values_at("a", "b") - assert_equal [1, 2], @strings.values_at(:a, :b) - assert_equal [1, 2], @symbols.values_at("a", "b") - assert_equal [1, 2], @symbols.values_at(:a, :b) - assert_equal [1, 2], @mixed.values_at("a", "b") - assert_equal [1, 2], @mixed.values_at(:a, :b) - end - - def test_indifferent_reading - hash = HashWithIndifferentAccess.new - hash["a"] = 1 - hash["b"] = true - hash["c"] = false - hash["d"] = nil - - assert_equal 1, hash[:a] - assert_equal true, hash[:b] - assert_equal false, hash[:c] - assert_equal nil, hash[:d] - assert_equal nil, hash[:e] - end - - def test_indifferent_reading_with_nonnil_default - hash = HashWithIndifferentAccess.new(1) - hash["a"] = 1 - hash["b"] = true - hash["c"] = false - hash["d"] = nil - - assert_equal 1, hash[:a] - assert_equal true, hash[:b] - assert_equal false, hash[:c] - assert_equal nil, hash[:d] - assert_equal 1, hash[:e] - end - - def test_indifferent_writing - hash = HashWithIndifferentAccess.new - hash[:a] = 1 - hash["b"] = 2 - hash[3] = 3 - - assert_equal hash["a"], 1 - assert_equal hash["b"], 2 - assert_equal hash[:a], 1 - assert_equal hash[:b], 2 - assert_equal hash[3], 3 - end - - def test_indifferent_update - hash = HashWithIndifferentAccess.new - hash[:a] = "a" - hash["b"] = "b" - - updated_with_strings = hash.update(@strings) - updated_with_symbols = hash.update(@symbols) - updated_with_mixed = hash.update(@mixed) - - assert_equal updated_with_strings[:a], 1 - assert_equal updated_with_strings["a"], 1 - assert_equal updated_with_strings["b"], 2 - - assert_equal updated_with_symbols[:a], 1 - assert_equal updated_with_symbols["b"], 2 - assert_equal updated_with_symbols[:b], 2 - - assert_equal updated_with_mixed[:a], 1 - assert_equal updated_with_mixed["b"], 2 - - assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 } - end - - def test_update_with_to_hash_conversion - hash = HashWithIndifferentAccess.new - hash.update HashByConversion.new(a: 1) - assert_equal hash["a"], 1 - end - - def test_indifferent_merging - hash = HashWithIndifferentAccess.new - hash[:a] = "failure" - hash["b"] = "failure" - - other = { "a" => 1, :b => 2 } - - merged = hash.merge(other) - - assert_equal HashWithIndifferentAccess, merged.class - assert_equal 1, merged[:a] - assert_equal 2, merged["b"] - - hash.update(other) - - assert_equal 1, hash[:a] - assert_equal 2, hash["b"] - end - - def test_merge_with_to_hash_conversion - hash = HashWithIndifferentAccess.new - merged = hash.merge HashByConversion.new(a: 1) - assert_equal merged["a"], 1 - end - - def test_indifferent_replace - hash = HashWithIndifferentAccess.new - hash[:a] = 42 - - replaced = hash.replace(b: 12) - - assert hash.key?("b") - assert !hash.key?(:a) - assert_equal 12, hash[:b] - assert_same hash, replaced - end - - def test_replace_with_to_hash_conversion - hash = HashWithIndifferentAccess.new - hash[:a] = 42 - - replaced = hash.replace(HashByConversion.new(b: 12)) - - assert hash.key?("b") - assert !hash.key?(:a) - assert_equal 12, hash[:b] - assert_same hash, replaced - end - - def test_indifferent_merging_with_block - hash = HashWithIndifferentAccess.new - hash[:a] = 1 - hash["b"] = 3 - - other = { "a" => 4, :b => 2, "c" => 10 } - - merged = hash.merge(other) { |key, old, new| old > new ? old : new } - - assert_equal HashWithIndifferentAccess, merged.class - assert_equal 4, merged[:a] - assert_equal 3, merged["b"] - assert_equal 10, merged[:c] - - other_indifferent = HashWithIndifferentAccess.new("a" => 9, :b => 2) - - merged = hash.merge(other_indifferent) { |key, old, new| old + new } - - assert_equal HashWithIndifferentAccess, merged.class - assert_equal 10, merged[:a] - assert_equal 5, merged[:b] - end - - def test_indifferent_reverse_merging - hash = HashWithIndifferentAccess.new key: :old_value - hash.reverse_merge! key: :new_value - assert_equal :old_value, hash[:key] - - hash = HashWithIndifferentAccess.new("some" => "value", "other" => "value") - hash.reverse_merge!(some: "noclobber", another: "clobber") - assert_equal "value", hash[:some] - assert_equal "clobber", hash[:another] - end - - def test_indifferent_deleting - get_hash = proc { { a: "foo" }.with_indifferent_access } - hash = get_hash.call - assert_equal hash.delete(:a), "foo" - assert_equal hash.delete(:a), nil - hash = get_hash.call - assert_equal hash.delete("a"), "foo" - assert_equal hash.delete("a"), nil - end - - def test_indifferent_select - hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k,v| v == 1 } - - assert_equal({ "a" => 1 }, hash) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash - end - - def test_indifferent_select_returns_enumerator - enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).select - assert_instance_of Enumerator, enum - end - - def test_indifferent_select_returns_a_hash_when_unchanged - hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k,v| true } - - assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash - end - - def test_indifferent_select_bang - indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) - indifferent_strings.select! { |k,v| v == 1 } - - assert_equal({ "a" => 1 }, indifferent_strings) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings - end - - def test_indifferent_reject - hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject { |k,v| v != 1 } - - assert_equal({ "a" => 1 }, hash) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash - end - - def test_indifferent_reject_returns_enumerator - enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject - assert_instance_of Enumerator, enum - end - - def test_indifferent_reject_bang - indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) - indifferent_strings.reject! { |k,v| v != 1 } - - assert_equal({ "a" => 1 }, indifferent_strings) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings - end - - def test_indifferent_to_hash - # Should convert to a Hash with String keys. - assert_equal @strings, @mixed.with_indifferent_access.to_hash - - # Should preserve the default value. - mixed_with_default = @mixed.dup - mixed_with_default.default = "1234" - roundtrip = mixed_with_default.with_indifferent_access.to_hash - assert_equal @strings, roundtrip - assert_equal "1234", roundtrip.default - - # Ensure nested hashes are not HashWithIndiffereneAccess - new_to_hash = @nested_mixed.with_indifferent_access.to_hash - assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) - assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) - assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess) - end - - def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access - hash = HashWithIndifferentAccess.new { |h, k| h[k] = [] } - hash[:a] << 1 - - assert_equal [1], hash[:a] - end - - def test_with_indifferent_access_has_no_side_effects_on_existing_hash - hash = { content: [{ :foo => :bar, "bar" => "baz" }] } - hash.with_indifferent_access - - assert_equal [:foo, "bar"], hash[:content].first.keys - end - - def test_indifferent_hash_with_array_of_hashes - hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] } }.with_indifferent_access - assert_equal "1", hash[:urls][:url].first[:address] - - hash = hash.to_hash - assert_not hash.instance_of?(HashWithIndifferentAccess) - assert_not hash["urls"].instance_of?(HashWithIndifferentAccess) - assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess) - end - - def test_should_preserve_array_subclass_when_value_is_array - array = SubclassingArray.new - array << { "address" => "1" } - hash = { "urls" => { "url" => array } }.with_indifferent_access - assert_equal SubclassingArray, hash[:urls][:url].class - end - - def test_should_preserve_array_class_when_hash_value_is_frozen_array - array = SubclassingArray.new - array << { "address" => "1" } - hash = { "urls" => { "url" => array.freeze } }.with_indifferent_access - assert_equal SubclassingArray, hash[:urls][:url].class - end - - def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash - h = HashWithIndifferentAccess.new - h[:first] = 1 - h = h.stringify_keys - assert_equal 1, h["first"] - h = HashWithIndifferentAccess.new - h["first"] = 1 - h = h.symbolize_keys - assert_equal 1, h[:first] - end - - def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash - h = HashWithIndifferentAccess.new - h[:first] = 1 - h = h.deep_stringify_keys - assert_equal 1, h["first"] - h = HashWithIndifferentAccess.new - h["first"] = 1 - h = h.deep_symbolize_keys - assert_equal 1, h[:first] - end - - def test_to_options_on_indifferent_preserves_hash - h = HashWithIndifferentAccess.new - h["first"] = 1 - h.to_options! - assert_equal 1, h["first"] - end - - def test_to_options_on_indifferent_preserves_works_as_hash_with_dup - h = HashWithIndifferentAccess.new(a: { b: "b" }) - dup = h.dup - - dup[:a][:c] = "c" - assert_equal "c", h[:a][:c] - end - - def test_indifferent_sub_hashes - h = { "user" => { "id" => 5 } }.with_indifferent_access - ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } - - h = { user: { id: 5 } }.with_indifferent_access - ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } - end - - def test_indifferent_duplication - # Should preserve default value - h = HashWithIndifferentAccess.new - h.default = "1234" - assert_equal h.default, h.dup.default - - # Should preserve class for subclasses - h = IndifferentHash.new - assert_equal h.class, h.dup.class - end - - def test_nested_dig_indifferent_access - skip if RUBY_VERSION < "2.3.0" - data = { "this" => { "views" => 1234 } }.with_indifferent_access - assert_equal 1234, data.dig(:this, :views) + assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed) end def test_assert_valid_keys @@ -739,12 +269,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message end - def test_assorted_keys_not_stringified - original = { Object.new => 2, 1 => 2, [] => true } - indiff = original.with_indifferent_access - assert(!indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!") - end - def test_deep_merge hash_1 = { a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } } } hash_2 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } @@ -759,9 +283,9 @@ class HashExtTest < ActiveSupport::TestCase hash_1 = { a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } } } hash_2 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } expected = { a: [:a, "a", 1], b: "b", c: { c1: [:c1, "c1", 2], c2: "c2", c3: { d1: "d1", d2: "d2" } } } - assert_equal(expected, hash_1.deep_merge(hash_2) { |k,o,n| [k, o, n] }) + assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] }) - hash_1.deep_merge!(hash_2) { |k,o,n| [k, o, n] } + hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] } assert_equal expected, hash_1 end @@ -775,37 +299,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end - def test_deep_merge_on_indifferent_access - hash_1 = HashWithIndifferentAccess.new(a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } }) - hash_2 = HashWithIndifferentAccess.new(a: 1, c: { c1: 2, c3: { d2: "d2" } }) - hash_3 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } - expected = { "a" => 1, "b" => "b", "c" => { "c1" => 2, "c2" => "c2", "c3" => { "d1" => "d1", "d2" => "d2" } } } - assert_equal expected, hash_1.deep_merge(hash_2) - assert_equal expected, hash_1.deep_merge(hash_3) - - hash_1.deep_merge!(hash_2) - assert_equal expected, hash_1 - end - - def test_store_on_indifferent_access - hash = HashWithIndifferentAccess.new - hash.store(:test1, 1) - hash.store("test1", 11) - hash[:test2] = 2 - hash["test2"] = 22 - expected = { "test1" => 11, "test2" => 22 } - assert_equal expected, hash - end - - def test_constructor_on_indifferent_access - hash = HashWithIndifferentAccess[:foo, 1] - assert_equal 1, hash[:foo] - assert_equal 1, hash["foo"] - hash[:foo] = 3 - assert_equal 3, hash[:foo] - assert_equal 3, hash["foo"] - end - def test_reverse_merge defaults = { a: "x", b: "y", c: 10 }.freeze options = { a: 1, b: 2 } @@ -826,6 +319,21 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, merged end + def test_with_defaults_aliases_reverse_merge + defaults = { a: "x", b: "y", c: 10 }.freeze + options = { a: 1, b: 2 } + expected = { a: 1, b: 2, c: 10 } + + # Should be an alias for reverse_merge + assert_equal expected, options.with_defaults(defaults) + assert_not_equal expected, options + + # Should be an alias for reverse_merge! + merged = options.dup + assert_equal expected, merged.with_defaults!(defaults) + assert_equal expected, merged + end + def test_slice original = { a: "x", b: "y", c: 10 } expected = { a: "x", b: "y" } @@ -868,38 +376,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, original.slice(*[:a, :b]) end - def test_indifferent_slice - original = { a: "x", b: "y", c: 10 }.with_indifferent_access - expected = { a: "x", b: "y" }.with_indifferent_access - - [["a", "b"], [:a, :b]].each do |keys| - # Should return a new hash with only the given keys. - assert_equal expected, original.slice(*keys), keys.inspect - assert_not_equal expected, original - end - end - - def test_indifferent_slice_inplace - original = { a: "x", b: "y", c: 10 }.with_indifferent_access - expected = { c: 10 }.with_indifferent_access - - [["a", "b"], [:a, :b]].each do |keys| - # Should replace the hash with only the given keys. - copy = original.dup - assert_equal expected, copy.slice!(*keys) - end - end - - def test_indifferent_slice_access_with_symbols - original = { "login" => "bender", "password" => "shiny", "stuff" => "foo" } - original = original.with_indifferent_access - - slice = original.slice(:login, :password) - - assert_equal "bender", slice[:login] - assert_equal "bender", slice["login"] - end - def test_slice_bang_does_not_override_default hash = Hash.new(0) hash.update(a: 1, b: 2) @@ -933,20 +409,8 @@ class HashExtTest < ActiveSupport::TestCase extracted = original.extract!(:a, :x) assert_equal expected, extracted - assert_equal nil, extracted[:a] - assert_equal nil, extracted[:x] - end - - def test_indifferent_extract - original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access - expected = { a: 1, b: 2 }.with_indifferent_access - remaining = { c: 3, d: 4 }.with_indifferent_access - - [["a", "b"], [:a, :b]].each do |keys| - copy = original.dup - assert_equal expected, copy.extract!(*keys) - assert_equal remaining, copy - end + assert_nil extracted[:a] + assert_nil extracted[:x] end def test_except @@ -1017,64 +481,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal({}, h) h = @symbols.dup - assert_equal(nil, h.compact!) + assert_nil(h.compact!) assert_equal(@symbols, h) end - - def test_new_with_to_hash_conversion - hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1)) - assert hash.key?("a") - assert_equal 1, hash[:a] - end - - def test_dup_with_default_proc - hash = HashWithIndifferentAccess.new - hash.default_proc = proc { |h, v| raise "walrus" } - assert_nothing_raised { hash.dup } - end - - def test_dup_with_default_proc_sets_proc - hash = HashWithIndifferentAccess.new - hash.default_proc = proc { |h, k| k + 1 } - new_hash = hash.dup - - assert_equal 3, new_hash[2] - - new_hash.default = 2 - assert_equal 2, new_hash[:non_existent] - end - - def test_to_hash_with_raising_default_proc - hash = HashWithIndifferentAccess.new - hash.default_proc = proc { |h, k| raise "walrus" } - - assert_nothing_raised { hash.to_hash } - end - - def test_new_from_hash_copying_default_should_not_raise_when_default_proc_does - hash = Hash.new - hash.default_proc = proc { |h, k| raise "walrus" } - - assert_deprecated { HashWithIndifferentAccess.new_from_hash_copying_default(hash) } - end - - def test_new_with_to_hash_conversion_copies_default - normal_hash = Hash.new(3) - normal_hash[:a] = 1 - - hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) - assert_equal 1, hash[:a] - assert_equal 3, hash[:b] - end - - def test_new_with_to_hash_conversion_copies_default_proc - normal_hash = Hash.new { 1 + 2 } - normal_hash[:a] = 1 - - hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) - assert_equal 1, hash[:a] - assert_equal 3, hash[:b] - end end class IWriteMyOwnXML @@ -1227,8 +636,8 @@ class HashToXmlTest < ActiveSupport::TestCase def test_timezoned_attributes xml = { - created_at: Time.utc(1999,2,2), - local_created_at: Time.utc(1999,2,2).in_time_zone("Eastern Time (US & Canada)") + created_at: Time.utc(1999, 2, 2), + local_created_at: Time.utc(1999, 2, 2).in_time_zone("Eastern Time (US & Canada)") }.to_xml(@xml_options) assert_match %r{<created-at type=\"dateTime\">1999-02-02T00:00:00Z</created-at>}, xml assert_match %r{<local-created-at type=\"dateTime\">1999-02-01T19:00:00-05:00</local-created-at>}, xml @@ -1537,7 +946,7 @@ class HashToXmlTest < ActiveSupport::TestCase weight: 0.5, chunky: true, price: BigDecimal("12.50"), - expires_at: Time.utc(2007,12,25,12,34,56), + expires_at: Time.utc(2007, 12, 25, 12, 34, 56), notes: "", illustration: "babe.png", caption: "That'll do, pig." @@ -1595,61 +1004,6 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>') end - def test_should_use_default_proc_for_unknown_key - hash_wia = HashWithIndifferentAccess.new { 1 + 2 } - assert_equal 3, hash_wia[:new_key] - end - - def test_should_return_nil_if_no_key_is_supplied - hash_wia = HashWithIndifferentAccess.new { 1 + 2 } - assert_equal nil, hash_wia.default - end - - def test_should_use_default_value_for_unknown_key - hash_wia = HashWithIndifferentAccess.new(3) - assert_equal 3, hash_wia[:new_key] - end - - def test_should_use_default_value_if_no_key_is_supplied - hash_wia = HashWithIndifferentAccess.new(3) - assert_equal 3, hash_wia.default - end - - def test_should_nil_if_no_default_value_is_supplied - hash_wia = HashWithIndifferentAccess.new - assert_nil hash_wia.default - end - - def test_should_return_dup_for_with_indifferent_access - hash_wia = HashWithIndifferentAccess.new - assert_equal hash_wia, hash_wia.with_indifferent_access - assert_not_same hash_wia, hash_wia.with_indifferent_access - end - - def test_allows_setting_frozen_array_values_with_indifferent_access - value = [1, 2, 3].freeze - hash = HashWithIndifferentAccess.new - hash[:key] = value - assert_equal hash[:key], value - end - - def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access - hash = Hash.new(3) - hash_wia = hash.with_indifferent_access - assert_equal 3, hash_wia.default - end - - def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access - hash = Hash.new do - 2 + 1 - end - assert_equal 3, hash[:foo] - - hash_wia = hash.with_indifferent_access - assert_equal 3, hash_wia[:foo] - assert_equal 3, hash_wia[:bar] - end - # The XML builder seems to fail miserably when trying to tag something # with the same name as a Kernel method (throw, test, loop, select ...) def test_kernel_method_names_to_xml diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index db0008b735..26f5088ede 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -49,19 +49,3 @@ class KernelSuppressTest < ActiveSupport::TestCase suppress(LoadError, ArgumentError) { raise ArgumentError } end end - -class MockStdErr - attr_reader :output - def puts(message) - @output ||= [] - @output << message - end - - def info(message) - puts(message) - end - - def write(message) - puts(message) - end -end diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb index b50a35b2cb..44ff6bb051 100644 --- a/activesupport/test/core_ext/load_error_test.rb +++ b/activesupport/test/core_ext/load_error_test.rb @@ -1,14 +1,6 @@ require "abstract_unit" require "active_support/core_ext/load_error" -class TestMissingSourceFile < ActiveSupport::TestCase - def test_it_is_deprecated - assert_deprecated do - MissingSourceFile.new - end - end -end - class TestLoadError < ActiveSupport::TestCase def test_with_require assert_raise(LoadError) { require 'no_this_file_don\'t_exist' } diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index a899f98705..cabeed2fae 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -19,6 +19,19 @@ class MarshalTest < ActiveSupport::TestCase end end + test "that Marshal#load still works when passed a proc" do + example_string = "test" + + example_proc = Proc.new do |o| + if o.is_a?(String) + o.capitalize! + end + end + + dumped = Marshal.dump(example_string) + assert_equal Marshal.load(dumped, example_proc), "Test" + end + test "that a missing class is autoloaded from string" do dumped = nil with_autoloading_fixtures do 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 b816fa50e3..af240bc38d 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 @@ -121,11 +121,11 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase def test_should_not_affect_superclass_if_subclass_set_value @class.foo = "super" - assert_equal @class.foo, "super" + assert_equal "super", @class.foo assert_nil @subclass.foo @subclass.foo = "sub" - assert_equal @class.foo, "super" - assert_equal @subclass.foo, "sub" + assert_equal "super", @class.foo + assert_equal "sub", @subclass.foo end end diff --git a/activesupport/test/core_ext/module/attribute_aliasing_test.rb b/activesupport/test/core_ext/module/attribute_aliasing_test.rb index d8c2dfd6b8..fdfa868851 100644 --- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb +++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb @@ -52,8 +52,8 @@ class AttributeAliasingTest < ActiveSupport::TestCase assert_equal "No, really, this is not a joke.", e.Data assert e.Data? - e.Data = "Uppercased methods are teh suck" - assert_equal "Uppercased methods are teh suck", e.body + e.Data = "Uppercased methods are the suck" + assert_equal "Uppercased methods are the suck", e.body assert e.body? end end diff --git a/activesupport/test/core_ext/module/introspection_test.rb b/activesupport/test/core_ext/module/introspection_test.rb new file mode 100644 index 0000000000..db383850cd --- /dev/null +++ b/activesupport/test/core_ext/module/introspection_test.rb @@ -0,0 +1,37 @@ +require "abstract_unit" +require "active_support/core_ext/module/introspection" + +module ParentA + module B + module C; end + module FrozenC; end + FrozenC.freeze + end + + module FrozenB; end + FrozenB.freeze +end + +class IntrospectionTest < ActiveSupport::TestCase + def test_parent_name + assert_equal "ParentA", ParentA::B.parent_name + assert_equal "ParentA::B", ParentA::B::C.parent_name + assert_nil ParentA.parent_name + end + + def test_parent_name_when_frozen + assert_equal "ParentA", ParentA::FrozenB.parent_name + assert_equal "ParentA::B", ParentA::B::FrozenC.parent_name + end + + def test_parent + assert_equal ParentA::B, ParentA::B::C.parent + assert_equal ParentA, ParentA::B.parent + assert_equal Object, ParentA.parent + end + + def test_parents + assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.parents + assert_equal [ParentA, Object], ParentA::B.parents + end +end diff --git a/activesupport/test/core_ext/module/qualified_const_test.rb b/activesupport/test/core_ext/module/qualified_const_test.rb deleted file mode 100644 index 418bc80ab9..0000000000 --- a/activesupport/test/core_ext/module/qualified_const_test.rb +++ /dev/null @@ -1,118 +0,0 @@ -require "abstract_unit" -require "active_support/core_ext/module/qualified_const" - -module QualifiedConstTestMod - X = false - - module M - X = 1 - - class C - X = 2 - end - end - - module N - include M - end -end - -class QualifiedConstTest < ActiveSupport::TestCase - test "Object.qualified_const_defined?" do - assert_deprecated do - assert Object.qualified_const_defined?("QualifiedConstTestMod") - assert !Object.qualified_const_defined?("NonExistingQualifiedConstTestMod") - - assert Object.qualified_const_defined?("QualifiedConstTestMod::X") - assert !Object.qualified_const_defined?("QualifiedConstTestMod::Y") - - assert Object.qualified_const_defined?("QualifiedConstTestMod::M::X") - assert !Object.qualified_const_defined?("QualifiedConstTestMod::M::Y") - - if Module.method(:const_defined?).arity == 1 - assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X") - else - assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X") - assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X", false) - assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X", true) - end - end - end - - test "mod.qualified_const_defined?" do - assert_deprecated do - assert QualifiedConstTestMod.qualified_const_defined?("M") - assert !QualifiedConstTestMod.qualified_const_defined?("NonExistingM") - - assert QualifiedConstTestMod.qualified_const_defined?("M::X") - assert !QualifiedConstTestMod.qualified_const_defined?("M::Y") - - assert QualifiedConstTestMod.qualified_const_defined?("M::C::X") - assert !QualifiedConstTestMod.qualified_const_defined?("M::C::Y") - - if Module.method(:const_defined?).arity == 1 - assert !QualifiedConstTestMod.qualified_const_defined?("QualifiedConstTestMod::N::X") - else - assert QualifiedConstTestMod.qualified_const_defined?("N::X") - assert !QualifiedConstTestMod.qualified_const_defined?("N::X", false) - assert QualifiedConstTestMod.qualified_const_defined?("N::X", true) - end - end - end - - test "qualified_const_get" do - assert_deprecated do - assert_equal false, Object.qualified_const_get("QualifiedConstTestMod::X") - assert_equal false, QualifiedConstTestMod.qualified_const_get("X") - assert_equal 1, QualifiedConstTestMod.qualified_const_get("M::X") - assert_equal 1, QualifiedConstTestMod.qualified_const_get("N::X") - assert_equal 2, QualifiedConstTestMod.qualified_const_get("M::C::X") - - assert_raise(NameError) { QualifiedConstTestMod.qualified_const_get("M::C::Y") } - end - end - - test "qualified_const_set" do - assert_deprecated do - begin - m = Module.new - assert_equal m, Object.qualified_const_set("QualifiedConstTestMod2", m) - assert_equal m, ::QualifiedConstTestMod2 - - # We are going to assign to existing constants on purpose, so silence warnings. - silence_warnings do - assert_equal true, QualifiedConstTestMod.qualified_const_set("QualifiedConstTestMod::X", true) - assert_equal true, QualifiedConstTestMod::X - - assert_equal 10, QualifiedConstTestMod::M.qualified_const_set("X", 10) - assert_equal 10, QualifiedConstTestMod::M::X - end - ensure - silence_warnings do - QualifiedConstTestMod.qualified_const_set("QualifiedConstTestMod::X", false) - QualifiedConstTestMod::M.qualified_const_set("X", 1) - end - end - end - end - - test "reject absolute paths" do - assert_deprecated do - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X") } - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y") } - - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X") } - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y") } - - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil) } - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil) } - end - end - - private - - def assert_raise_with_message(expected_exception, expected_message, &block) - exception = assert_raise(expected_exception, &block) - assert_equal expected_message, exception.message - end -end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 36073b28b7..a4d4444d69 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -1,30 +1,11 @@ require "abstract_unit" require "active_support/core_ext/module" -module One - Constant1 = "Hello World" - Constant2 = "What's up?" -end - -class Ab - include One - Constant1 = "Hello World" # Will have different object id than One::Constant1 - Constant3 = "Goodbye World" -end - -module Yz - module Zy - class Cd - include One - end - end -end - Somewhere = Struct.new(:street, :city) do attr_accessor :name end -class Someone < Struct.new(:name, :place) +Someone = Struct.new(:name, :place) do delegate :street, :city, :to_f, to: :place delegate :name=, to: :place, prefix: true delegate :upcase, to: "place.city" @@ -35,10 +16,10 @@ class Someone < Struct.new(:name, :place) "some_table" end - FAILED_DELEGATE_LINE = __LINE__ + 1 + self::FAILED_DELEGATE_LINE = __LINE__ + 1 delegate :foo, to: :place - FAILED_DELEGATE_LINE_2 = __LINE__ + 1 + self::FAILED_DELEGATE_LINE_2 = __LINE__ + 1 delegate :bar, to: :place, allow_nil: true private @@ -48,12 +29,12 @@ class Someone < Struct.new(:name, :place) end end -Invoice = Struct.new(:client) do +Invoice = Struct.new(:client) do delegate :street, :city, :name, to: :client, prefix: true delegate :street, :city, :name, to: :client, prefix: :customer end -Project = Struct.new(:description, :person) do +Project = Struct.new(:description, :person) do delegate :name, to: :person, allow_nil: true delegate :to_f, to: :description, allow_nil: true end @@ -89,7 +70,23 @@ Product = Struct.new(:name) do end end +module ExtraMissing + def method_missing(sym, *args) + if sym == :extra_missing + 42 + else + super + end + end + + def respond_to_missing?(sym, priv = false) + sym == :extra_missing || super + end +end + DecoratedTester = Struct.new(:client) do + include ExtraMissing + delegate_missing_to :client end @@ -210,21 +207,21 @@ class ModuleTest < ActiveSupport::TestCase def test_delegation_prefix invoice = Invoice.new(@david) - assert_equal invoice.client_name, "David" - assert_equal invoice.client_street, "Paulina" - assert_equal invoice.client_city, "Chicago" + assert_equal "David", invoice.client_name + assert_equal "Paulina", invoice.client_street + assert_equal "Chicago", invoice.client_city end def test_delegation_custom_prefix invoice = Invoice.new(@david) - assert_equal invoice.customer_name, "David" - assert_equal invoice.customer_street, "Paulina" - assert_equal invoice.customer_city, "Chicago" + assert_equal "David", invoice.customer_name + assert_equal "Paulina", invoice.customer_street + assert_equal "Chicago", invoice.customer_city end def test_delegation_prefix_with_nil_or_false - assert_equal Developer.new(@david).name, "David" - assert_equal Tester.new(@david).name, "David" + assert_equal "David", Developer.new(@david).name + assert_equal "David", Tester.new(@david).name end def test_delegation_prefix_with_instance_variable @@ -240,7 +237,7 @@ class ModuleTest < ActiveSupport::TestCase def test_delegation_with_allow_nil rails = Project.new("Rails", Someone.new("David")) - assert_equal rails.name, "David" + assert_equal "David", rails.name end def test_delegation_with_allow_nil_and_nil_value @@ -351,15 +348,15 @@ class ModuleTest < ActiveSupport::TestCase assert has_block.hello? end - def test_delegate_to_missing_with_method + def test_delegate_missing_to_with_method assert_equal "David", DecoratedTester.new(@david).name end - def test_delegate_to_missing_with_reserved_methods + def test_delegate_missing_to_with_reserved_methods assert_equal "David", DecoratedReserved.new(@david).name end - def test_delegate_to_missing_does_not_delegate_to_private_methods + def test_delegate_missing_to_does_not_delegate_to_private_methods e = assert_raises(NoMethodError) do DecoratedReserved.new(@david).private_name end @@ -367,7 +364,7 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `private_name' for/, e.message) end - def test_delegate_to_missing_does_not_delegate_to_fake_methods + def test_delegate_missing_to_does_not_delegate_to_fake_methods e = assert_raises(NoMethodError) do DecoratedReserved.new(@david).my_fake_method end @@ -375,240 +372,20 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end - def test_parent - assert_equal Yz::Zy, Yz::Zy::Cd.parent - assert_equal Yz, Yz::Zy.parent - assert_equal Object, Yz.parent - end - - def test_parents - assert_equal [Yz::Zy, Yz, Object], Yz::Zy::Cd.parents - assert_equal [Yz, Object], Yz::Zy.parents - end - - def test_local_constants - ActiveSupport::Deprecation.silence do - assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s) - end - end - - def test_local_constants_is_deprecated - assert_deprecated { Ab.local_constants.sort.map(&:to_s) } - end -end - -module BarMethodAliaser - def self.included(foo_class) - foo_class.class_eval do - include BarMethods - alias_method_chain :bar, :baz - end - end -end - -module BarMethods - def bar_with_baz - bar_without_baz << "_with_baz" - end - - def quux_with_baz! - quux_without_baz! << "_with_baz" - end - - def quux_with_baz? - false - end - - def quux_with_baz=(v) - send(:quux_without_baz=, v) << "_with_baz" - end - - def duck_with_orange - duck_without_orange << "_with_orange" - end -end - -class MethodAliasingTest < ActiveSupport::TestCase - def setup - Object.const_set :FooClassWithBarMethod, Class.new { def bar() "bar" end } - @instance = FooClassWithBarMethod.new - end - - def teardown - Object.instance_eval { remove_const :FooClassWithBarMethod } - end - - def test_alias_method_chain_deprecated - assert_deprecated(/alias_method_chain/) do - Module.new do - def base - end - - def base_with_deprecated - end - - alias_method_chain :base, :deprecated - end - end - end - - def test_alias_method_chain - assert_deprecated(/alias_method_chain/) do - assert @instance.respond_to?(:bar) - feature_aliases = [:bar_with_baz, :bar_without_baz] - - feature_aliases.each do |method| - assert !@instance.respond_to?(method) - end - - assert_equal "bar", @instance.bar - - FooClassWithBarMethod.class_eval { include BarMethodAliaser } - - feature_aliases.each do |method| - assert_respond_to @instance, method - end - - assert_equal "bar_with_baz", @instance.bar - assert_equal "bar", @instance.bar_without_baz - end - end - - def test_alias_method_chain_with_punctuation_method - assert_deprecated(/alias_method_chain/) do - FooClassWithBarMethod.class_eval do - def quux!; "quux" end - end - - assert !@instance.respond_to?(:quux_with_baz!) - FooClassWithBarMethod.class_eval do - include BarMethodAliaser - alias_method_chain :quux!, :baz - end - assert_respond_to @instance, :quux_with_baz! - - assert_equal "quux_with_baz", @instance.quux! - assert_equal "quux", @instance.quux_without_baz! - end - end - - def test_alias_method_chain_with_same_names_between_predicates_and_bang_methods - assert_deprecated(/alias_method_chain/) do - FooClassWithBarMethod.class_eval do - def quux!; "quux!" end - def quux?; true end - def quux=(v); "quux=" end - end - - assert !@instance.respond_to?(:quux_with_baz!) - assert !@instance.respond_to?(:quux_with_baz?) - assert !@instance.respond_to?(:quux_with_baz=) - - FooClassWithBarMethod.class_eval { include BarMethodAliaser } - assert_respond_to @instance, :quux_with_baz! - assert_respond_to @instance, :quux_with_baz? - assert_respond_to @instance, :quux_with_baz= - - FooClassWithBarMethod.alias_method_chain :quux!, :baz - assert_equal "quux!_with_baz", @instance.quux! - assert_equal "quux!", @instance.quux_without_baz! - - FooClassWithBarMethod.alias_method_chain :quux?, :baz - assert_equal false, @instance.quux? - assert_equal true, @instance.quux_without_baz? - - FooClassWithBarMethod.alias_method_chain :quux=, :baz - assert_equal "quux=_with_baz", @instance.send(:quux=, 1234) - assert_equal "quux=", @instance.send(:quux_without_baz=, 1234) - end - end - - def test_alias_method_chain_with_feature_punctuation - assert_deprecated(/alias_method_chain/) do - FooClassWithBarMethod.class_eval do - def quux; "quux" end - def quux?; "quux?" end - include BarMethodAliaser - alias_method_chain :quux, :baz! - end - - assert_nothing_raised do - assert_equal "quux_with_baz", @instance.quux_with_baz! - end - - assert_raise(NameError) do - FooClassWithBarMethod.alias_method_chain :quux?, :baz! - end - end - end - - def test_alias_method_chain_yields_target_and_punctuation - assert_deprecated(/alias_method_chain/) do - args = nil - - FooClassWithBarMethod.class_eval do - def quux?; end - include BarMethods + 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) - FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| - args = [target, punctuation] - end - end - - assert_not_nil args - assert_equal "quux", args[0] - assert_equal "?", args[1] - end + assert DecoratedTester.new(@david).respond_to?(:name, true) + assert_not DecoratedTester.new(@david).respond_to?(:private_name, true) + assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true) end - def test_alias_method_chain_preserves_private_method_status - assert_deprecated(/alias_method_chain/) do - FooClassWithBarMethod.class_eval do - def duck; "duck" end - include BarMethodAliaser - private :duck - alias_method_chain :duck, :orange - end - - assert_raise NoMethodError do - @instance.duck - end + def test_delegate_missing_to_respects_superclass_missing + assert_equal 42, DecoratedTester.new(@david).extra_missing - assert_equal "duck_with_orange", @instance.instance_eval { duck } - assert FooClassWithBarMethod.private_method_defined?(:duck) - end - end - - def test_alias_method_chain_preserves_protected_method_status - assert_deprecated(/alias_method_chain/) do - FooClassWithBarMethod.class_eval do - def duck; "duck" end - include BarMethodAliaser - protected :duck - alias_method_chain :duck, :orange - end - - assert_raise NoMethodError do - @instance.duck - end - - assert_equal "duck_with_orange", @instance.instance_eval { duck } - assert FooClassWithBarMethod.protected_method_defined?(:duck) - end - end - - def test_alias_method_chain_preserves_public_method_status - assert_deprecated(/alias_method_chain/) do - FooClassWithBarMethod.class_eval do - def duck; "duck" end - include BarMethodAliaser - public :duck - alias_method_chain :duck, :orange - end - - assert_equal "duck_with_orange", @instance.duck - assert FooClassWithBarMethod.public_method_defined?(:duck) - end + assert_respond_to DecoratedTester.new(@david), :extra_missing end def test_delegate_with_case diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 5e824747ed..3cfbe6e7e6 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -5,14 +5,14 @@ require "active_support/core_ext/integer" class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase def setup - @now = Time.local(2005,2,10,15,30,45) - @dtnow = DateTime.civil(2005,2,10,15,30,45) + @now = Time.local(2005, 2, 10, 15, 30, 45) + @dtnow = DateTime.civil(2005, 2, 10, 15, 30, 45) @seconds = { 1.minute => 60, 10.minutes => 600, 1.hour + 15.minutes => 4500, 2.days + 4.hours + 30.minutes => 189000, - 5.years + 1.month + 1.fortnight => 161589600 + 5.years + 1.month + 1.fortnight => 161624106 } end @@ -61,15 +61,15 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase end def test_duration_after_conversion_is_no_longer_accurate - assert_equal 30.days.to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now) - assert_equal 365.25.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now) - assert_equal 30.days.to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow) - assert_equal 365.25.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow) + assert_equal (1.year / 12).to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now) + assert_equal 365.2425.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now) + assert_equal (1.year / 12).to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow) + assert_equal 365.2425.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow) end def test_add_one_year_to_leap_day - assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year - assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year + assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10) + 1.year + assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10) + 1.year end end @@ -83,7 +83,7 @@ class NumericExtDateTest < ActiveSupport::TestCase assert_equal @today >> 1, @today + 1.month assert_equal @today.to_time.since(1), @today + 1.second assert_equal @today.to_time.since(60), @today + 1.minute - assert_equal @today.to_time.since(60*60), @today + 1.hour + assert_equal @today.to_time.since(60 * 60), @today + 1.hour end def test_chaining_duration_operations @@ -92,7 +92,7 @@ class NumericExtDateTest < ActiveSupport::TestCase end def test_add_one_year_to_leap_day - assert_equal Date.new(2005,2,28), Date.new(2004,2,29) + 1.year + assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29) + 1.year end end @@ -102,12 +102,12 @@ class NumericExtSizeTest < ActiveSupport::TestCase assert_equal 1024.kilobytes, 1.megabyte assert_equal 3584.0.kilobytes, 3.5.megabytes assert_equal 3584.0.megabytes, 3.5.gigabytes - assert_equal 1.kilobyte ** 4, 1.terabyte + assert_equal 1.kilobyte**4, 1.terabyte assert_equal 1024.kilobytes + 2.megabytes, 3.megabytes assert_equal 2.gigabytes / 4, 512.megabytes assert_equal 256.megabytes * 20 + 5.gigabytes, 10.gigabytes - assert_equal 1.kilobyte ** 5, 1.petabyte - assert_equal 1.kilobyte ** 6, 1.exabyte + assert_equal 1.kilobyte**5, 1.petabyte + assert_equal 1.kilobyte**6, 1.exabyte end def test_units_as_bytes_independently @@ -228,37 +228,37 @@ class NumericExtFormattingTest < ActiveSupport::TestCase def test_to_s__rounded__with_significant_digits assert_equal "124000", 123987.to_s(:rounded, precision: 3, significant: true) - assert_equal "120000000", 123987876.to_s(:rounded, precision: 2, significant: true ) - assert_equal "9775", 9775.to_s(:rounded, precision: 4, significant: true ) - assert_equal "5.4", 5.3923.to_s(:rounded, precision: 2, significant: true ) - assert_equal "5", 5.3923.to_s(:rounded, precision: 1, significant: true ) - assert_equal "1", 1.232.to_s(:rounded, precision: 1, significant: true ) - assert_equal "7", 7.to_s(:rounded, precision: 1, significant: true ) - assert_equal "1", 1.to_s(:rounded, precision: 1, significant: true ) - assert_equal "53", 52.7923.to_s(:rounded, precision: 2, significant: true ) - assert_equal "9775.00", 9775.to_s(:rounded, precision: 6, significant: true ) - assert_equal "5.392900", 5.3929.to_s(:rounded, precision: 7, significant: true ) - assert_equal "0.0", 0.to_s(:rounded, precision: 2, significant: true ) - assert_equal "0", 0.to_s(:rounded, precision: 1, significant: true ) - assert_equal "0.0001", 0.0001.to_s(:rounded, precision: 1, significant: true ) - assert_equal "0.000100", 0.0001.to_s(:rounded, precision: 3, significant: true ) - assert_equal "0.0001", 0.0001111.to_s(:rounded, precision: 1, significant: true ) + assert_equal "120000000", 123987876.to_s(:rounded, precision: 2, significant: true) + assert_equal "9775", 9775.to_s(:rounded, precision: 4, significant: true) + assert_equal "5.4", 5.3923.to_s(:rounded, precision: 2, significant: true) + assert_equal "5", 5.3923.to_s(:rounded, precision: 1, significant: true) + assert_equal "1", 1.232.to_s(:rounded, precision: 1, significant: true) + assert_equal "7", 7.to_s(:rounded, precision: 1, significant: true) + assert_equal "1", 1.to_s(:rounded, precision: 1, significant: true) + assert_equal "53", 52.7923.to_s(:rounded, precision: 2, significant: true) + assert_equal "9775.00", 9775.to_s(:rounded, precision: 6, significant: true) + assert_equal "5.392900", 5.3929.to_s(:rounded, precision: 7, significant: true) + assert_equal "0.0", 0.to_s(:rounded, precision: 2, significant: true) + assert_equal "0", 0.to_s(:rounded, precision: 1, significant: true) + assert_equal "0.0001", 0.0001.to_s(:rounded, precision: 1, significant: true) + assert_equal "0.000100", 0.0001.to_s(:rounded, precision: 3, significant: true) + assert_equal "0.0001", 0.0001111.to_s(:rounded, precision: 1, significant: true) assert_equal "10.0", 9.995.to_s(:rounded, precision: 3, significant: true) assert_equal "9.99", 9.994.to_s(:rounded, precision: 3, significant: true) assert_equal "11.0", 10.995.to_s(:rounded, precision: 3, significant: true) end def test_to_s__rounded__with_strip_insignificant_zeros - assert_equal "9775.43", 9775.43.to_s(:rounded, precision: 4, strip_insignificant_zeros: true ) - assert_equal "9775.2", 9775.2.to_s(:rounded, precision: 6, significant: true, strip_insignificant_zeros: true ) - assert_equal "0", 0.to_s(:rounded, precision: 6, significant: true, strip_insignificant_zeros: true ) + assert_equal "9775.43", 9775.43.to_s(:rounded, precision: 4, strip_insignificant_zeros: true) + assert_equal "9775.2", 9775.2.to_s(:rounded, precision: 6, significant: true, strip_insignificant_zeros: true) + assert_equal "0", 0.to_s(:rounded, precision: 6, significant: true, strip_insignificant_zeros: true) end def test_to_s__rounded__with_significant_true_and_zero_precision # Zero precision with significant is a mistake (would always return zero), # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) assert_equal "124", 123.987.to_s(:rounded, precision: 0, significant: true) - assert_equal "12", 12.to_s(:rounded, precision: 0, significant: true ) + assert_equal "12", 12.to_s(:rounded, precision: 0, significant: true) end def test_to_s__human_size @@ -287,21 +287,6 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "10 Bytes", 10.to_s(:human_size) end - def test_to_s__human_size_with_si_prefix - assert_deprecated do - assert_equal "3 Bytes", 3.14159265.to_s(:human_size, prefix: :si) - assert_equal "123 Bytes", 123.0.to_s(:human_size, prefix: :si) - assert_equal "123 Bytes", 123.to_s(:human_size, prefix: :si) - assert_equal "1.23 KB", 1234.to_s(:human_size, prefix: :si) - assert_equal "12.3 KB", 12345.to_s(:human_size, prefix: :si) - assert_equal "1.23 MB", 1234567.to_s(:human_size, prefix: :si) - assert_equal "1.23 GB", 1234567890.to_s(:human_size, prefix: :si) - assert_equal "1.23 TB", 1234567890123.to_s(:human_size, prefix: :si) - assert_equal "1.23 PB", 1234567890123456.to_s(:human_size, prefix: :si) - assert_equal "1.23 EB", 1234567890123456789.to_s(:human_size, prefix: :si) - end - end - def test_to_s__human_size_with_options_hash assert_equal "1.2 MB", 1234567.to_s(:human_size, precision: 2) assert_equal "3 Bytes", 3.14159265.to_s(:human_size, precision: 4) @@ -391,12 +376,6 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "1 Million", BigDecimal("1000010").to_s(:human) end - def test_to_formatted_s_is_deprecated - assert_deprecated do - 5551234.to_formatted_s(:phone) - end - end - def test_to_s_with_invalid_formatter assert_equal "123", 123.to_s(:invalid) assert_equal "2.5", 2.5.to_s(:invalid) @@ -415,6 +394,10 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "1000010.0", BigDecimal("1000010").to_s assert_equal "10000 10.0", BigDecimal("1000010").to_s("5F") + + assert_raises TypeError do + 1.to_s({}) + end end def test_in_milliseconds @@ -423,7 +406,6 @@ class NumericExtFormattingTest < ActiveSupport::TestCase # TODO: Remove positive and negative tests when we drop support to ruby < 2.3 b = 2**64 - b *= b until Bignum === b T_ZERO = b.coerce(0).first T_ONE = b.coerce(1).first @@ -453,8 +435,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase end.new assert_not_predicate(a, :positive?) - assert_predicate(1/2r, :positive?) - assert_not_predicate(-1/2r, :positive?) + assert_predicate(1 / 2r, :positive?) + assert_not_predicate(-1 / 2r, :positive?) assert_predicate(T_ONE, :positive?) assert_not_predicate(T_MONE, :positive?) @@ -491,8 +473,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase end.new assert_not_predicate(a, :negative?) - assert_predicate(-1/2r, :negative?) - assert_not_predicate(1/2r, :negative?) + assert_predicate(-1 / 2r, :negative?) + assert_not_predicate(1 / 2r, :negative?) assert_not_predicate(T_ONE, :negative?) assert_predicate(T_MONE, :negative?) diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index ab0676524e..7fd3fed042 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -15,7 +15,7 @@ class BlankTest < ActiveSupport::TestCase end BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ] - NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 } ] + NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now ] def test_blank BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" } @@ -28,7 +28,7 @@ class BlankTest < ActiveSupport::TestCase end def test_presence - BLANK.each { |v| assert_equal nil, v.presence, "#{v.inspect}.presence should return nil" } + BLANK.each { |v| assert_nil v.presence, "#{v.inspect}.presence should return nil" } NOT.each { |v| assert_equal v, v.presence, "#{v.inspect}.presence should return self" } end end diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index e335ec1b40..f247ee16de 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -6,7 +6,7 @@ class DeepDupTest < ActiveSupport::TestCase array = [1, [2, 3]] dup = array.deep_dup dup[1][2] = 4 - assert_equal nil, array[1][2] + assert_nil array[1][2] assert_equal 4, dup[1][2] end @@ -14,7 +14,7 @@ class DeepDupTest < ActiveSupport::TestCase hash = { a: { b: "b" } } dup = hash.deep_dup dup[:a][:c] = "c" - assert_equal nil, hash[:a][:c] + assert_nil hash[:a][:c] assert_equal "c", dup[:a][:c] end @@ -22,7 +22,7 @@ class DeepDupTest < ActiveSupport::TestCase array = [1, { a: 2, b: 3 } ] dup = array.deep_dup dup[1][:c] = 4 - assert_equal nil, array[1][:c] + assert_nil array[1][:c] assert_equal 4, dup[1][:c] end @@ -30,7 +30,7 @@ class DeepDupTest < ActiveSupport::TestCase hash = { a: [1, 2] } dup = hash.deep_dup dup[:a][2] = "c" - assert_equal nil, hash[:a][2] + assert_nil hash[:a][2] assert_equal "c", dup[:a][2] end diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index 2cbfefe235..68b0129980 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -4,16 +4,26 @@ require "active_support/core_ext/object/duplicable" require "active_support/core_ext/numeric/time" class DuplicableTest < ActiveSupport::TestCase - RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts)] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new] - ALLOW_DUP << BigDecimal.new("4.56") + 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)] + 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] + 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] + 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")] + end def test_duplicable rubinius_skip "* Method#dup is allowed at the moment on Rubinius\n" \ "* https://github.com/rubinius/rubinius/issues/3089" RAISE_DUP.each do |v| - assert !v.duplicable? + assert !v.duplicable?, "#{ v.inspect } should not be duplicable" assert_raises(TypeError, v.class.name) { v.dup } end diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index be211ec7dc..955686d6aa 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -3,8 +3,8 @@ require "active_support/core_ext/object/inclusion" class InTest < ActiveSupport::TestCase def test_in_array - assert 1.in?([1,2]) - assert !3.in?([1,2]) + assert 1.in?([1, 2]) + assert !3.in?([1, 2]) end def test_in_hash @@ -25,7 +25,7 @@ class InTest < ActiveSupport::TestCase end def test_in_set - s = Set.new([1,2]) + s = Set.new([1, 2]) assert 1.in?(s) assert !3.in?(s) end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index fc0e72e09a..d166c7309c 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -99,21 +99,21 @@ 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_raises TypeError do ((twz - 1.hour)..twz).include?(twz) end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 4b40503d22..a98951e889 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -1,5 +1,6 @@ require "date" require "abstract_unit" +require "timeout" require "inflector_test_cases" require "constantize_test_cases" @@ -77,6 +78,12 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_titleize_with_keep_id_suffix + MixtureToTitleCaseWithKeepIdSuffix.each do |before, titleized| + assert_equal(titleized, before.titleize(keep_id_suffix: true)) + end + end + def test_upcase_first assert_equal "What a Lovely Day", "what a Lovely Day".upcase_first end @@ -152,53 +159,37 @@ class StringInflectionsTest < ActiveSupport::TestCase def test_string_parameterized_normal StringToParameterized.each do |normal, slugged| - assert_equal(normal.parameterize, slugged) + assert_equal(slugged, normal.parameterize) end end def test_string_parameterized_normal_preserve_case StringToParameterizedPreserveCase.each do |normal, slugged| - assert_equal(normal.parameterize(preserve_case: true), slugged) + assert_equal(slugged, normal.parameterize(preserve_case: true)) end end def test_string_parameterized_no_separator StringToParameterizeWithNoSeparator.each do |normal, slugged| - assert_equal(normal.parameterize(separator: ""), slugged) - end - end - - def test_string_parameterized_no_separator_deprecated - StringToParameterizeWithNoSeparator.each do |normal, slugged| - assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: ''` instead./i) do - assert_equal(normal.parameterize(""), slugged) - end + assert_equal(slugged, normal.parameterize(separator: "")) end end def test_string_parameterized_no_separator_preserve_case StringToParameterizePreserveCaseWithNoSeparator.each do |normal, slugged| - assert_equal(normal.parameterize(separator: "", preserve_case: true), slugged) + assert_equal(slugged, normal.parameterize(separator: "", preserve_case: true)) end end def test_string_parameterized_underscore StringToParameterizeWithUnderscore.each do |normal, slugged| - assert_equal(normal.parameterize(separator: "_"), slugged) - end - end - - def test_string_parameterized_underscore_deprecated - StringToParameterizeWithUnderscore.each do |normal, slugged| - assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do - assert_equal(normal.parameterize("_"), slugged) - end + assert_equal(slugged, normal.parameterize(separator: "_")) end end def test_string_parameterized_underscore_preserve_case StringToParameterizePreserceCaseWithUnderscore.each do |normal, slugged| - assert_equal(normal.parameterize(separator: "_", preserve_case: true), slugged) + assert_equal(slugged, normal.parameterize(separator: "_", preserve_case: true)) end end @@ -214,6 +205,12 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_humanize_with_keep_id_suffix + UnderscoreToHumanWithKeepIdSuffix.each do |underscore, human| + assert_equal(human, underscore.humanize(keep_id_suffix: true)) + end + end + def test_humanize_with_html_escape assert_equal "Hello", ERB::Util.html_escape("hello").humanize end @@ -238,7 +235,7 @@ class StringInflectionsTest < ActiveSupport::TestCase original = %{\u205f\u3000 A string surrounded by various unicode spaces, with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007} - expected = "A string surrounded by various unicode spaces, " + + expected = "A string surrounded by various unicode spaces, " \ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." # Make sure squish returns what we expect: @@ -299,7 +296,7 @@ class StringInflectionsTest < ActiveSupport::TestCase def test_truncate_words_with_complex_string Timeout.timeout(10) do complex_string = "aa aa aaa aa aaa aaa aaa aa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaaa aaaaa aaaaa aaaaaa aa aa aa aaa aa aaa aa aa aa aa a aaa aaa \n a aaa <<s" - assert_equal complex_string.truncate_words(80), complex_string + assert_equal complex_string, complex_string.truncate_words(80) end rescue Timeout::Error assert false @@ -355,7 +352,7 @@ class StringAccessTest < ActiveSupport::TestCase test "#at with Regex, returns the matching portion of the string" do assert_equal "lo", "hello".at(/lo/) - assert_equal nil, "hello".at(/nonexisting/) + assert_nil "hello".at(/nonexisting/) end test "#from with positive Integer, returns substring from the given position to the end" do @@ -726,14 +723,14 @@ class OutputSafetyTest < ActiveSupport::TestCase test "Prepending safe onto unsafe yields unsafe" do @string.prepend "other".html_safe assert !@string.html_safe? - assert_equal @string, "otherhello" + 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_equal other, "<foo>other" + assert_equal "<foo>other", other end test "Concatting safe onto unsafe yields unsafe" do diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index a6c9a7d5a8..625a5bffb8 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -4,32 +4,32 @@ require "core_ext/date_and_time_behavior" require "time_zone_test_helpers" class TimeExtCalculationsTest < ActiveSupport::TestCase - def date_time_init(year,month,day,hour,minute,second,usec=0) - Time.local(year,month,day,hour,minute,second,usec) + def date_time_init(year, month, day, hour, minute, second, usec = 0) + Time.local(year, month, day, hour, minute, second, usec) end include DateAndTimeBehavior include TimeZoneTestHelpers def test_seconds_since_midnight - assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight - assert_equal 60,Time.local(2005,1,1,0,1,0).seconds_since_midnight - assert_equal 3660,Time.local(2005,1,1,1,1,0).seconds_since_midnight - assert_equal 86399,Time.local(2005,1,1,23,59,59).seconds_since_midnight - assert_equal 60.00001,Time.local(2005,1,1,0,1,0,10).seconds_since_midnight + assert_equal 1, Time.local(2005, 1, 1, 0, 0, 1).seconds_since_midnight + assert_equal 60, Time.local(2005, 1, 1, 0, 1, 0).seconds_since_midnight + assert_equal 3660, Time.local(2005, 1, 1, 1, 1, 0).seconds_since_midnight + assert_equal 86399, Time.local(2005, 1, 1, 23, 59, 59).seconds_since_midnight + assert_equal 60.00001, Time.local(2005, 1, 1, 0, 1, 0, 10).seconds_since_midnight end def test_seconds_since_midnight_at_daylight_savings_time_start with_env_tz "US/Eastern" do # dt: US: 2005 April 3rd 2:00am ST => April 3rd 3:00am DT - assert_equal 2*3600-1, Time.local(2005,4,3,1,59,59).seconds_since_midnight, "just before DST start" - assert_equal 2*3600+1, Time.local(2005,4,3,3, 0, 1).seconds_since_midnight, "just after DST start" + assert_equal 2 * 3600 - 1, Time.local(2005, 4, 3, 1, 59, 59).seconds_since_midnight, "just before DST start" + assert_equal 2 * 3600 + 1, Time.local(2005, 4, 3, 3, 0, 1).seconds_since_midnight, "just after DST start" end with_env_tz "NZ" do # dt: New Zealand: 2006 October 1st 2:00am ST => October 1st 3:00am DT - assert_equal 2*3600-1, Time.local(2006,10,1,1,59,59).seconds_since_midnight, "just before DST start" - assert_equal 2*3600+1, Time.local(2006,10,1,3, 0, 1).seconds_since_midnight, "just after DST start" + assert_equal 2 * 3600 - 1, Time.local(2006, 10, 1, 1, 59, 59).seconds_since_midnight, "just before DST start" + assert_equal 2 * 3600 + 1, Time.local(2006, 10, 1, 3, 0, 1).seconds_since_midnight, "just after DST start" end end @@ -37,47 +37,47 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase with_env_tz "US/Eastern" do # st: US: 2005 October 30th 2:00am DT => October 30th 1:00am ST # avoid setting a time between 1:00 and 2:00 since that requires specifying whether DST is active - assert_equal 1*3600-1, Time.local(2005,10,30,0,59,59).seconds_since_midnight, "just before DST end" - assert_equal 3*3600+1, Time.local(2005,10,30,2, 0, 1).seconds_since_midnight, "just after DST end" + assert_equal 1 * 3600 - 1, Time.local(2005, 10, 30, 0, 59, 59).seconds_since_midnight, "just before DST end" + assert_equal 3 * 3600 + 1, Time.local(2005, 10, 30, 2, 0, 1).seconds_since_midnight, "just after DST end" # now set a time between 1:00 and 2:00 by specifying whether DST is active # uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz ) - assert_equal 1*3600+30*60, Time.local(0,30,1,30,10,2005,0,0,true,ENV["TZ"]).seconds_since_midnight, "before DST end" - assert_equal 2*3600+30*60, Time.local(0,30,1,30,10,2005,0,0,false,ENV["TZ"]).seconds_since_midnight, "after DST end" + assert_equal 1 * 3600 + 30 * 60, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, true, ENV["TZ"]).seconds_since_midnight, "before DST end" + assert_equal 2 * 3600 + 30 * 60, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, false, ENV["TZ"]).seconds_since_midnight, "after DST end" end with_env_tz "NZ" do # st: New Zealand: 2006 March 19th 3:00am DT => March 19th 2:00am ST # avoid setting a time between 2:00 and 3:00 since that requires specifying whether DST is active - assert_equal 2*3600-1, Time.local(2006,3,19,1,59,59).seconds_since_midnight, "just before DST end" - assert_equal 4*3600+1, Time.local(2006,3,19,3, 0, 1).seconds_since_midnight, "just after DST end" + assert_equal 2 * 3600 - 1, Time.local(2006, 3, 19, 1, 59, 59).seconds_since_midnight, "just before DST end" + assert_equal 4 * 3600 + 1, Time.local(2006, 3, 19, 3, 0, 1).seconds_since_midnight, "just after DST end" # now set a time between 2:00 and 3:00 by specifying whether DST is active # uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz ) - assert_equal 2*3600+30*60, Time.local(0,30,2,19,3,2006,0,0,true, ENV["TZ"]).seconds_since_midnight, "before DST end" - assert_equal 3*3600+30*60, Time.local(0,30,2,19,3,2006,0,0,false,ENV["TZ"]).seconds_since_midnight, "after DST end" + assert_equal 2 * 3600 + 30 * 60, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, true, ENV["TZ"]).seconds_since_midnight, "before DST end" + assert_equal 3 * 3600 + 30 * 60, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, false, ENV["TZ"]).seconds_since_midnight, "after DST end" end end def test_seconds_until_end_of_day - assert_equal 0, Time.local(2005,1,1,23,59,59).seconds_until_end_of_day - assert_equal 1, Time.local(2005,1,1,23,59,58).seconds_until_end_of_day - assert_equal 60, Time.local(2005,1,1,23,58,59).seconds_until_end_of_day - assert_equal 3660, Time.local(2005,1,1,22,58,59).seconds_until_end_of_day - assert_equal 86399, Time.local(2005,1,1,0,0,0).seconds_until_end_of_day + assert_equal 0, Time.local(2005, 1, 1, 23, 59, 59).seconds_until_end_of_day + assert_equal 1, Time.local(2005, 1, 1, 23, 59, 58).seconds_until_end_of_day + assert_equal 60, Time.local(2005, 1, 1, 23, 58, 59).seconds_until_end_of_day + assert_equal 3660, Time.local(2005, 1, 1, 22, 58, 59).seconds_until_end_of_day + assert_equal 86399, Time.local(2005, 1, 1, 0, 0, 0).seconds_until_end_of_day end def test_seconds_until_end_of_day_at_daylight_savings_time_start with_env_tz "US/Eastern" do # dt: US: 2005 April 3rd 2:00am ST => April 3rd 3:00am DT - assert_equal 21*3600, Time.local(2005,4,3,1,59,59).seconds_until_end_of_day, "just before DST start" - assert_equal 21*3600-2, Time.local(2005,4,3,3,0,1).seconds_until_end_of_day, "just after DST start" + assert_equal 21 * 3600, Time.local(2005, 4, 3, 1, 59, 59).seconds_until_end_of_day, "just before DST start" + assert_equal 21 * 3600 - 2, Time.local(2005, 4, 3, 3, 0, 1).seconds_until_end_of_day, "just after DST start" end with_env_tz "NZ" do # dt: New Zealand: 2006 October 1st 2:00am ST => October 1st 3:00am DT - assert_equal 21*3600, Time.local(2006,10,1,1,59,59).seconds_until_end_of_day, "just before DST start" - assert_equal 21*3600-2, Time.local(2006,10,1,3,0,1).seconds_until_end_of_day, "just after DST start" + assert_equal 21 * 3600, Time.local(2006, 10, 1, 1, 59, 59).seconds_until_end_of_day, "just before DST start" + assert_equal 21 * 3600 - 2, Time.local(2006, 10, 1, 3, 0, 1).seconds_until_end_of_day, "just after DST start" end end @@ -85,83 +85,83 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase with_env_tz "US/Eastern" do # st: US: 2005 October 30th 2:00am DT => October 30th 1:00am ST # avoid setting a time between 1:00 and 2:00 since that requires specifying whether DST is active - assert_equal 24*3600, Time.local(2005,10,30,0,59,59).seconds_until_end_of_day, "just before DST end" - assert_equal 22*3600-2, Time.local(2005,10,30,2,0,1).seconds_until_end_of_day, "just after DST end" + assert_equal 24 * 3600, Time.local(2005, 10, 30, 0, 59, 59).seconds_until_end_of_day, "just before DST end" + assert_equal 22 * 3600 - 2, Time.local(2005, 10, 30, 2, 0, 1).seconds_until_end_of_day, "just after DST end" # now set a time between 1:00 and 2:00 by specifying whether DST is active # uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz ) - assert_equal 24*3600-30*60-1, Time.local(0,30,1,30,10,2005,0,0,true,ENV["TZ"]).seconds_until_end_of_day, "before DST end" - assert_equal 23*3600-30*60-1, Time.local(0,30,1,30,10,2005,0,0,false,ENV["TZ"]).seconds_until_end_of_day, "after DST end" + assert_equal 24 * 3600 - 30 * 60 - 1, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, true, ENV["TZ"]).seconds_until_end_of_day, "before DST end" + assert_equal 23 * 3600 - 30 * 60 - 1, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, false, ENV["TZ"]).seconds_until_end_of_day, "after DST end" end with_env_tz "NZ" do # st: New Zealand: 2006 March 19th 3:00am DT => March 19th 2:00am ST # avoid setting a time between 2:00 and 3:00 since that requires specifying whether DST is active - assert_equal 23*3600, Time.local(2006,3,19,1,59,59).seconds_until_end_of_day, "just before DST end" - assert_equal 21*3600-2, Time.local(2006,3,19,3,0,1).seconds_until_end_of_day, "just after DST end" + assert_equal 23 * 3600, Time.local(2006, 3, 19, 1, 59, 59).seconds_until_end_of_day, "just before DST end" + assert_equal 21 * 3600 - 2, Time.local(2006, 3, 19, 3, 0, 1).seconds_until_end_of_day, "just after DST end" # now set a time between 2:00 and 3:00 by specifying whether DST is active # uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz ) - assert_equal 23*3600-30*60-1, Time.local(0,30,2,19,3,2006,0,0,true, ENV["TZ"]).seconds_until_end_of_day, "before DST end" - assert_equal 22*3600-30*60-1, Time.local(0,30,2,19,3,2006,0,0,false,ENV["TZ"]).seconds_until_end_of_day, "after DST end" + assert_equal 23 * 3600 - 30 * 60 - 1, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, true, ENV["TZ"]).seconds_until_end_of_day, "before DST end" + assert_equal 22 * 3600 - 30 * 60 - 1, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, false, ENV["TZ"]).seconds_until_end_of_day, "after DST end" end end def test_sec_fraction - time = Time.utc(2016, 4, 23, 0, 0, Rational(1,10000000000)) - assert_equal Rational(1,10000000000), time.sec_fraction + time = Time.utc(2016, 4, 23, 0, 0, Rational(1, 10000000000)) + assert_equal Rational(1, 10000000000), time.sec_fraction time = Time.utc(2016, 4, 23, 0, 0, 0.0000000001) assert_equal 0.0000000001.to_r, time.sec_fraction - time = Time.utc(2016, 4, 23, 0, 0, 0, Rational(1,10000)) - assert_equal Rational(1,10000000000), time.sec_fraction + time = Time.utc(2016, 4, 23, 0, 0, 0, Rational(1, 10000)) + assert_equal Rational(1, 10000000000), time.sec_fraction time = Time.utc(2016, 4, 23, 0, 0, 0, 0.0001) assert_equal 0.0001.to_r / 1000000, time.sec_fraction end def test_beginning_of_day - assert_equal Time.local(2005,2,4,0,0,0), Time.local(2005,2,4,10,10,10).beginning_of_day + assert_equal Time.local(2005, 2, 4, 0, 0, 0), Time.local(2005, 2, 4, 10, 10, 10).beginning_of_day with_env_tz "US/Eastern" do - assert_equal Time.local(2006,4,2,0,0,0), Time.local(2006,4,2,10,10,10).beginning_of_day, "start DST" - assert_equal Time.local(2006,10,29,0,0,0), Time.local(2006,10,29,10,10,10).beginning_of_day, "ends DST" + assert_equal Time.local(2006, 4, 2, 0, 0, 0), Time.local(2006, 4, 2, 10, 10, 10).beginning_of_day, "start DST" + assert_equal Time.local(2006, 10, 29, 0, 0, 0), Time.local(2006, 10, 29, 10, 10, 10).beginning_of_day, "ends DST" end with_env_tz "NZ" do - assert_equal Time.local(2006,3,19,0,0,0), Time.local(2006,3,19,10,10,10).beginning_of_day, "ends DST" - assert_equal Time.local(2006,10,1,0,0,0), Time.local(2006,10,1,10,10,10).beginning_of_day, "start DST" + assert_equal Time.local(2006, 3, 19, 0, 0, 0), Time.local(2006, 3, 19, 10, 10, 10).beginning_of_day, "ends DST" + assert_equal Time.local(2006, 10, 1, 0, 0, 0), Time.local(2006, 10, 1, 10, 10, 10).beginning_of_day, "start DST" end end def test_middle_of_day - assert_equal Time.local(2005,2,4,12,0,0), Time.local(2005,2,4,10,10,10).middle_of_day + assert_equal Time.local(2005, 2, 4, 12, 0, 0), Time.local(2005, 2, 4, 10, 10, 10).middle_of_day with_env_tz "US/Eastern" do - assert_equal Time.local(2006,4,2,12,0,0), Time.local(2006,4,2,10,10,10).middle_of_day, "start DST" - assert_equal Time.local(2006,10,29,12,0,0), Time.local(2006,10,29,10,10,10).middle_of_day, "ends DST" + assert_equal Time.local(2006, 4, 2, 12, 0, 0), Time.local(2006, 4, 2, 10, 10, 10).middle_of_day, "start DST" + assert_equal Time.local(2006, 10, 29, 12, 0, 0), Time.local(2006, 10, 29, 10, 10, 10).middle_of_day, "ends DST" end with_env_tz "NZ" do - assert_equal Time.local(2006,3,19,12,0,0), Time.local(2006,3,19,10,10,10).middle_of_day, "ends DST" - assert_equal Time.local(2006,10,1,12,0,0), Time.local(2006,10,1,10,10,10).middle_of_day, "start DST" + assert_equal Time.local(2006, 3, 19, 12, 0, 0), Time.local(2006, 3, 19, 10, 10, 10).middle_of_day, "ends DST" + assert_equal Time.local(2006, 10, 1, 12, 0, 0), Time.local(2006, 10, 1, 10, 10, 10).middle_of_day, "start DST" end end def test_beginning_of_hour - assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour + assert_equal Time.local(2005, 2, 4, 19, 0, 0), Time.local(2005, 2, 4, 19, 30, 10).beginning_of_hour end def test_beginning_of_minute - assert_equal Time.local(2005,2,4,19,30,0), Time.local(2005,2,4,19,30,10).beginning_of_minute + assert_equal Time.local(2005, 2, 4, 19, 30, 0), Time.local(2005, 2, 4, 19, 30, 10).beginning_of_minute end def test_end_of_day - assert_equal Time.local(2007,8,12,23,59,59,Rational(999999999, 1000)), Time.local(2007,8,12,10,10,10).end_of_day + assert_equal Time.local(2007, 8, 12, 23, 59, 59, Rational(999999999, 1000)), Time.local(2007, 8, 12, 10, 10, 10).end_of_day with_env_tz "US/Eastern" do - assert_equal Time.local(2007,4,2,23,59,59,Rational(999999999, 1000)), Time.local(2007,4,2,10,10,10).end_of_day, "start DST" - assert_equal Time.local(2007,10,29,23,59,59,Rational(999999999, 1000)), Time.local(2007,10,29,10,10,10).end_of_day, "ends DST" + assert_equal Time.local(2007, 4, 2, 23, 59, 59, Rational(999999999, 1000)), Time.local(2007, 4, 2, 10, 10, 10).end_of_day, "start DST" + assert_equal Time.local(2007, 10, 29, 23, 59, 59, Rational(999999999, 1000)), Time.local(2007, 10, 29, 10, 10, 10).end_of_day, "ends DST" end with_env_tz "NZ" do - assert_equal Time.local(2006,3,19,23,59,59,Rational(999999999, 1000)), Time.local(2006,3,19,10,10,10).end_of_day, "ends DST" - assert_equal Time.local(2006,10,1,23,59,59,Rational(999999999, 1000)), Time.local(2006,10,1,10,10,10).end_of_day, "start DST" + assert_equal Time.local(2006, 3, 19, 23, 59, 59, Rational(999999999, 1000)), Time.local(2006, 3, 19, 10, 10, 10).end_of_day, "ends DST" + assert_equal Time.local(2006, 10, 1, 23, 59, 59, Rational(999999999, 1000)), Time.local(2006, 10, 1, 10, 10, 10).end_of_day, "start DST" end with_env_tz "Asia/Yekaterinburg" do assert_equal Time.local(2015, 2, 8, 23, 59, 59, Rational(999999999, 1000)), Time.new(2015, 2, 8, 8, 0, 0, "+05:00").end_of_day @@ -169,334 +169,341 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_end_of_hour - assert_equal Time.local(2005,2,4,19,59,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_hour + assert_equal Time.local(2005, 2, 4, 19, 59, 59, Rational(999999999, 1000)), Time.local(2005, 2, 4, 19, 30, 10).end_of_hour end def test_end_of_minute - assert_equal Time.local(2005,2,4,19,30,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_minute + assert_equal Time.local(2005, 2, 4, 19, 30, 59, Rational(999999999, 1000)), Time.local(2005, 2, 4, 19, 30, 10).end_of_minute end def test_last_year - assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).last_year + assert_equal Time.local(2004, 6, 5, 10), Time.local(2005, 6, 5, 10, 0, 0).last_year end def test_ago - assert_equal Time.local(2005,2,22,10,10,9), Time.local(2005,2,22,10,10,10).ago(1) - assert_equal Time.local(2005,2,22,9,10,10), Time.local(2005,2,22,10,10,10).ago(3600) - assert_equal Time.local(2005,2,20,10,10,10), Time.local(2005,2,22,10,10,10).ago(86400*2) - assert_equal Time.local(2005,2,20,9,9,45), Time.local(2005,2,22,10,10,10).ago(86400*2 + 3600 + 25) + assert_equal Time.local(2005, 2, 22, 10, 10, 9), Time.local(2005, 2, 22, 10, 10, 10).ago(1) + assert_equal Time.local(2005, 2, 22, 9, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).ago(3600) + assert_equal Time.local(2005, 2, 20, 10, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).ago(86400 * 2) + assert_equal Time.local(2005, 2, 20, 9, 9, 45), Time.local(2005, 2, 22, 10, 10, 10).ago(86400 * 2 + 3600 + 25) end def test_daylight_savings_time_crossings_backward_start with_env_tz "US/Eastern" do # dt: US: 2005 April 3rd 4:18am - assert_equal Time.local(2005,4,2,3,18,0), Time.local(2005,4,3,4,18,0).ago(24.hours), "dt-24.hours=>st" - assert_equal Time.local(2005,4,2,3,18,0), Time.local(2005,4,3,4,18,0).ago(86400), "dt-86400=>st" - assert_equal Time.local(2005,4,2,3,18,0), Time.local(2005,4,3,4,18,0).ago(86400.seconds), "dt-86400.seconds=>st" + assert_equal Time.local(2005, 4, 2, 3, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(24.hours), "dt-24.hours=>st" + assert_equal Time.local(2005, 4, 2, 3, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(86400), "dt-86400=>st" + assert_equal Time.local(2005, 4, 2, 3, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(86400.seconds), "dt-86400.seconds=>st" - assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(24.hours), "st-24.hours=>st" - assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(86400), "st-86400=>st" - assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(86400.seconds), "st-86400.seconds=>st" + assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(24.hours), "st-24.hours=>st" + assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(86400), "st-86400=>st" + assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(86400.seconds), "st-86400.seconds=>st" end with_env_tz "NZ" do # dt: New Zealand: 2006 October 1st 4:18am - assert_equal Time.local(2006,9,30,3,18,0), Time.local(2006,10,1,4,18,0).ago(24.hours), "dt-24.hours=>st" - assert_equal Time.local(2006,9,30,3,18,0), Time.local(2006,10,1,4,18,0).ago(86400), "dt-86400=>st" - assert_equal Time.local(2006,9,30,3,18,0), Time.local(2006,10,1,4,18,0).ago(86400.seconds), "dt-86400.seconds=>st" + assert_equal Time.local(2006, 9, 30, 3, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(24.hours), "dt-24.hours=>st" + assert_equal Time.local(2006, 9, 30, 3, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(86400), "dt-86400=>st" + assert_equal Time.local(2006, 9, 30, 3, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(86400.seconds), "dt-86400.seconds=>st" - assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(24.hours), "st-24.hours=>st" - assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(86400), "st-86400=>st" - assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(86400.seconds), "st-86400.seconds=>st" + assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(24.hours), "st-24.hours=>st" + assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(86400), "st-86400=>st" + assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(86400.seconds), "st-86400.seconds=>st" end end def test_daylight_savings_time_crossings_backward_end with_env_tz "US/Eastern" do # st: US: 2005 October 30th 4:03am - assert_equal Time.local(2005,10,29,5,3), Time.local(2005,10,30,4,3,0).ago(24.hours), "st-24.hours=>dt" - assert_equal Time.local(2005,10,29,5,3), Time.local(2005,10,30,4,3,0).ago(86400), "st-86400=>dt" - assert_equal Time.local(2005,10,29,5,3), Time.local(2005,10,30,4,3,0).ago(86400.seconds), "st-86400.seconds=>dt" + assert_equal Time.local(2005, 10, 29, 5, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(24.hours), "st-24.hours=>dt" + assert_equal Time.local(2005, 10, 29, 5, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(86400), "st-86400=>dt" + assert_equal Time.local(2005, 10, 29, 5, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(86400.seconds), "st-86400.seconds=>dt" - assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(24.hours), "dt-24.hours=>dt" - assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(86400), "dt-86400=>dt" - assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(86400.seconds), "dt-86400.seconds=>dt" + assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(24.hours), "dt-24.hours=>dt" + assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(86400), "dt-86400=>dt" + assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(86400.seconds), "dt-86400.seconds=>dt" end with_env_tz "NZ" do # st: New Zealand: 2006 March 19th 4:03am - assert_equal Time.local(2006,3,18,5,3), Time.local(2006,3,19,4,3,0).ago(24.hours), "st-24.hours=>dt" - assert_equal Time.local(2006,3,18,5,3), Time.local(2006,3,19,4,3,0).ago(86400), "st-86400=>dt" - assert_equal Time.local(2006,3,18,5,3), Time.local(2006,3,19,4,3,0).ago(86400.seconds), "st-86400.seconds=>dt" + assert_equal Time.local(2006, 3, 18, 5, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(24.hours), "st-24.hours=>dt" + assert_equal Time.local(2006, 3, 18, 5, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(86400), "st-86400=>dt" + assert_equal Time.local(2006, 3, 18, 5, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(86400.seconds), "st-86400.seconds=>dt" - assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(24.hours), "dt-24.hours=>dt" - assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(86400), "dt-86400=>dt" - assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(86400.seconds), "dt-86400.seconds=>dt" + assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(24.hours), "dt-24.hours=>dt" + assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(86400), "dt-86400=>dt" + assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(86400.seconds), "dt-86400.seconds=>dt" end end def test_daylight_savings_time_crossings_backward_start_1day with_env_tz "US/Eastern" do # dt: US: 2005 April 3rd 4:18am - assert_equal Time.local(2005,4,2,4,18,0), Time.local(2005,4,3,4,18,0).ago(1.day), "dt-1.day=>st" - assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(1.day), "st-1.day=>st" + assert_equal Time.local(2005, 4, 2, 4, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(1.day), "dt-1.day=>st" + assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(1.day), "st-1.day=>st" end with_env_tz "NZ" do # dt: New Zealand: 2006 October 1st 4:18am - assert_equal Time.local(2006,9,30,4,18,0), Time.local(2006,10,1,4,18,0).ago(1.day), "dt-1.day=>st" - assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(1.day), "st-1.day=>st" + assert_equal Time.local(2006, 9, 30, 4, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(1.day), "dt-1.day=>st" + assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(1.day), "st-1.day=>st" end end def test_daylight_savings_time_crossings_backward_end_1day with_env_tz "US/Eastern" do # st: US: 2005 October 30th 4:03am - assert_equal Time.local(2005,10,29,4,3), Time.local(2005,10,30,4,3,0).ago(1.day), "st-1.day=>dt" - assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(1.day), "dt-1.day=>dt" + assert_equal Time.local(2005, 10, 29, 4, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(1.day), "st-1.day=>dt" + assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(1.day), "dt-1.day=>dt" end with_env_tz "NZ" do # st: New Zealand: 2006 March 19th 4:03am - assert_equal Time.local(2006,3,18,4,3), Time.local(2006,3,19,4,3,0).ago(1.day), "st-1.day=>dt" - assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(1.day), "dt-1.day=>dt" + assert_equal Time.local(2006, 3, 18, 4, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(1.day), "st-1.day=>dt" + assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(1.day), "dt-1.day=>dt" end end def test_since - assert_equal Time.local(2005,2,22,10,10,11), Time.local(2005,2,22,10,10,10).since(1) - assert_equal Time.local(2005,2,22,11,10,10), Time.local(2005,2,22,10,10,10).since(3600) - assert_equal Time.local(2005,2,24,10,10,10), Time.local(2005,2,22,10,10,10).since(86400*2) - assert_equal Time.local(2005,2,24,11,10,35), Time.local(2005,2,22,10,10,10).since(86400*2 + 3600 + 25) + assert_equal Time.local(2005, 2, 22, 10, 10, 11), Time.local(2005, 2, 22, 10, 10, 10).since(1) + assert_equal Time.local(2005, 2, 22, 11, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).since(3600) + assert_equal Time.local(2005, 2, 24, 10, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).since(86400 * 2) + assert_equal Time.local(2005, 2, 24, 11, 10, 35), Time.local(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25) # when out of range of Time, returns a DateTime - assert_equal DateTime.civil(2038,1,20,11,59,59), Time.utc(2038,1,18,11,59,59).since(86400*2) + assert_equal DateTime.civil(2038, 1, 20, 11, 59, 59), Time.utc(2038, 1, 18, 11, 59, 59).since(86400 * 2) end def test_daylight_savings_time_crossings_forward_start with_env_tz "US/Eastern" do # st: US: 2005 April 2nd 7:27pm - assert_equal Time.local(2005,4,3,20,27,0), Time.local(2005,4,2,19,27,0).since(24.hours), "st+24.hours=>dt" - assert_equal Time.local(2005,4,3,20,27,0), Time.local(2005,4,2,19,27,0).since(86400), "st+86400=>dt" - assert_equal Time.local(2005,4,3,20,27,0), Time.local(2005,4,2,19,27,0).since(86400.seconds), "st+86400.seconds=>dt" + assert_equal Time.local(2005, 4, 3, 20, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(24.hours), "st+24.hours=>dt" + assert_equal Time.local(2005, 4, 3, 20, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(86400), "st+86400=>dt" + assert_equal Time.local(2005, 4, 3, 20, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(86400.seconds), "st+86400.seconds=>dt" - assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(24.hours), "dt+24.hours=>dt" - assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(86400), "dt+86400=>dt" - assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(86400.seconds), "dt+86400.seconds=>dt" + assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(24.hours), "dt+24.hours=>dt" + assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(86400), "dt+86400=>dt" + assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(86400.seconds), "dt+86400.seconds=>dt" end with_env_tz "NZ" do # st: New Zealand: 2006 September 30th 7:27pm - assert_equal Time.local(2006,10,1,20,27,0), Time.local(2006,9,30,19,27,0).since(24.hours), "st+24.hours=>dt" - assert_equal Time.local(2006,10,1,20,27,0), Time.local(2006,9,30,19,27,0).since(86400), "st+86400=>dt" - assert_equal Time.local(2006,10,1,20,27,0), Time.local(2006,9,30,19,27,0).since(86400.seconds), "st+86400.seconds=>dt" + assert_equal Time.local(2006, 10, 1, 20, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(24.hours), "st+24.hours=>dt" + assert_equal Time.local(2006, 10, 1, 20, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(86400), "st+86400=>dt" + assert_equal Time.local(2006, 10, 1, 20, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(86400.seconds), "st+86400.seconds=>dt" - assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(24.hours), "dt+24.hours=>dt" - assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(86400), "dt+86400=>dt" - assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(86400.seconds), "dt+86400.seconds=>dt" + assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(24.hours), "dt+24.hours=>dt" + assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(86400), "dt+86400=>dt" + assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(86400.seconds), "dt+86400.seconds=>dt" end end def test_daylight_savings_time_crossings_forward_start_1day with_env_tz "US/Eastern" do # st: US: 2005 April 2nd 7:27pm - assert_equal Time.local(2005,4,3,19,27,0), Time.local(2005,4,2,19,27,0).since(1.day), "st+1.day=>dt" - assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(1.day), "dt+1.day=>dt" + assert_equal Time.local(2005, 4, 3, 19, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(1.day), "st+1.day=>dt" + assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(1.day), "dt+1.day=>dt" end with_env_tz "NZ" do # st: New Zealand: 2006 September 30th 7:27pm - assert_equal Time.local(2006,10,1,19,27,0), Time.local(2006,9,30,19,27,0).since(1.day), "st+1.day=>dt" - assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(1.day), "dt+1.day=>dt" + assert_equal Time.local(2006, 10, 1, 19, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(1.day), "st+1.day=>dt" + assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(1.day), "dt+1.day=>dt" end end def test_daylight_savings_time_crossings_forward_start_tomorrow with_env_tz "US/Eastern" do # st: US: 2005 April 2nd 7:27pm - assert_equal Time.local(2005,4,3,19,27,0), Time.local(2005,4,2,19,27,0).tomorrow, "st+1.day=>dt" - assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).tomorrow, "dt+1.day=>dt" + assert_equal Time.local(2005, 4, 3, 19, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).tomorrow, "st+1.day=>dt" + assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).tomorrow, "dt+1.day=>dt" end with_env_tz "NZ" do # st: New Zealand: 2006 September 30th 7:27pm - assert_equal Time.local(2006,10,1,19,27,0), Time.local(2006,9,30,19,27,0).tomorrow, "st+1.day=>dt" - assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).tomorrow, "dt+1.day=>dt" + assert_equal Time.local(2006, 10, 1, 19, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).tomorrow, "st+1.day=>dt" + assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).tomorrow, "dt+1.day=>dt" end end def test_daylight_savings_time_crossings_backward_start_yesterday with_env_tz "US/Eastern" do # st: US: 2005 April 2nd 7:27pm - assert_equal Time.local(2005,4,2,19,27,0), Time.local(2005,4,3,19,27,0).yesterday, "dt-1.day=>st" - assert_equal Time.local(2005,4,3,19,27,0), Time.local(2005,4,4,19,27,0).yesterday, "dt-1.day=>dt" + assert_equal Time.local(2005, 4, 2, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).yesterday, "dt-1.day=>st" + assert_equal Time.local(2005, 4, 3, 19, 27, 0), Time.local(2005, 4, 4, 19, 27, 0).yesterday, "dt-1.day=>dt" end with_env_tz "NZ" do # st: New Zealand: 2006 September 30th 7:27pm - assert_equal Time.local(2006,9,30,19,27,0), Time.local(2006,10,1,19,27,0).yesterday, "dt-1.day=>st" - assert_equal Time.local(2006,10,1,19,27,0), Time.local(2006,10,2,19,27,0).yesterday, "dt-1.day=>dt" + assert_equal Time.local(2006, 9, 30, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).yesterday, "dt-1.day=>st" + assert_equal Time.local(2006, 10, 1, 19, 27, 0), Time.local(2006, 10, 2, 19, 27, 0).yesterday, "dt-1.day=>dt" end end def test_daylight_savings_time_crossings_forward_end with_env_tz "US/Eastern" do # dt: US: 2005 October 30th 12:45am - assert_equal Time.local(2005,10,30,23,45,0), Time.local(2005,10,30,0,45,0).since(24.hours), "dt+24.hours=>st" - assert_equal Time.local(2005,10,30,23,45,0), Time.local(2005,10,30,0,45,0).since(86400), "dt+86400=>st" - assert_equal Time.local(2005,10,30,23,45,0), Time.local(2005,10,30,0,45,0).since(86400.seconds), "dt+86400.seconds=>st" + assert_equal Time.local(2005, 10, 30, 23, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(24.hours), "dt+24.hours=>st" + assert_equal Time.local(2005, 10, 30, 23, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(86400), "dt+86400=>st" + assert_equal Time.local(2005, 10, 30, 23, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(86400.seconds), "dt+86400.seconds=>st" - assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(24.hours), "st+24.hours=>st" - assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(86400), "st+86400=>st" - assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(86400.seconds), "st+86400.seconds=>st" + assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(24.hours), "st+24.hours=>st" + assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(86400), "st+86400=>st" + assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(86400.seconds), "st+86400.seconds=>st" end with_env_tz "NZ" do # dt: New Zealand: 2006 March 19th 1:45am - assert_equal Time.local(2006,3,20,0,45,0), Time.local(2006,3,19,1,45,0).since(24.hours), "dt+24.hours=>st" - assert_equal Time.local(2006,3,20,0,45,0), Time.local(2006,3,19,1,45,0).since(86400), "dt+86400=>st" - assert_equal Time.local(2006,3,20,0,45,0), Time.local(2006,3,19,1,45,0).since(86400.seconds), "dt+86400.seconds=>st" + assert_equal Time.local(2006, 3, 20, 0, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(24.hours), "dt+24.hours=>st" + assert_equal Time.local(2006, 3, 20, 0, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(86400), "dt+86400=>st" + assert_equal Time.local(2006, 3, 20, 0, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(86400.seconds), "dt+86400.seconds=>st" - assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(24.hours), "st+24.hours=>st" - assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(86400), "st+86400=>st" - assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(86400.seconds), "st+86400.seconds=>st" + assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(24.hours), "st+24.hours=>st" + assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(86400), "st+86400=>st" + assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(86400.seconds), "st+86400.seconds=>st" end end def test_daylight_savings_time_crossings_forward_end_1day with_env_tz "US/Eastern" do # dt: US: 2005 October 30th 12:45am - assert_equal Time.local(2005,10,31,0,45,0), Time.local(2005,10,30,0,45,0).since(1.day), "dt+1.day=>st" - assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(1.day), "st+1.day=>st" + assert_equal Time.local(2005, 10, 31, 0, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(1.day), "dt+1.day=>st" + assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(1.day), "st+1.day=>st" end with_env_tz "NZ" do # dt: New Zealand: 2006 March 19th 1:45am - assert_equal Time.local(2006,3,20,1,45,0), Time.local(2006,3,19,1,45,0).since(1.day), "dt+1.day=>st" - assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(1.day), "st+1.day=>st" + assert_equal Time.local(2006, 3, 20, 1, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(1.day), "dt+1.day=>st" + assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(1.day), "st+1.day=>st" end end def test_daylight_savings_time_crossings_forward_end_tomorrow with_env_tz "US/Eastern" do # dt: US: 2005 October 30th 12:45am - assert_equal Time.local(2005,10,31,0,45,0), Time.local(2005,10,30,0,45,0).tomorrow, "dt+1.day=>st" - assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).tomorrow, "st+1.day=>st" + assert_equal Time.local(2005, 10, 31, 0, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).tomorrow, "dt+1.day=>st" + assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).tomorrow, "st+1.day=>st" end with_env_tz "NZ" do # dt: New Zealand: 2006 March 19th 1:45am - assert_equal Time.local(2006,3,20,1,45,0), Time.local(2006,3,19,1,45,0).tomorrow, "dt+1.day=>st" - assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).tomorrow, "st+1.day=>st" + assert_equal Time.local(2006, 3, 20, 1, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).tomorrow, "dt+1.day=>st" + assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).tomorrow, "st+1.day=>st" end end def test_daylight_savings_time_crossings_backward_end_yesterday with_env_tz "US/Eastern" do # dt: US: 2005 October 30th 12:45am - assert_equal Time.local(2005,10,30,0,45,0), Time.local(2005,10,31,0,45,0).yesterday, "st-1.day=>dt" - assert_equal Time.local(2005,10, 31,0,45,0), Time.local(2005,11,1,0,45,0).yesterday, "st-1.day=>st" + assert_equal Time.local(2005, 10, 30, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).yesterday, "st-1.day=>dt" + assert_equal Time.local(2005, 10, 31, 0, 45, 0), Time.local(2005, 11, 1, 0, 45, 0).yesterday, "st-1.day=>st" end with_env_tz "NZ" do # dt: New Zealand: 2006 March 19th 1:45am - assert_equal Time.local(2006,3,19,1,45,0), Time.local(2006,3,20,1,45,0).yesterday, "st-1.day=>dt" - assert_equal Time.local(2006,3,20,1,45,0), Time.local(2006,3,21,1,45,0).yesterday, "st-1.day=>st" + assert_equal Time.local(2006, 3, 19, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).yesterday, "st-1.day=>dt" + assert_equal Time.local(2006, 3, 20, 1, 45, 0), Time.local(2006, 3, 21, 1, 45, 0).yesterday, "st-1.day=>st" end end def test_change - assert_equal Time.local(2006,2,22,15,15,10), Time.local(2005,2,22,15,15,10).change(year: 2006) - assert_equal Time.local(2005,6,22,15,15,10), Time.local(2005,2,22,15,15,10).change(month: 6) - assert_equal Time.local(2012,9,22,15,15,10), Time.local(2005,2,22,15,15,10).change(year: 2012, month: 9) - assert_equal Time.local(2005,2,22,16), Time.local(2005,2,22,15,15,10).change(hour: 16) - assert_equal Time.local(2005,2,22,16,45), Time.local(2005,2,22,15,15,10).change(hour: 16, min: 45) - assert_equal Time.local(2005,2,22,15,45), Time.local(2005,2,22,15,15,10).change(min: 45) - - assert_equal Time.local(2005,1,2, 5, 0, 0, 0), Time.local(2005,1,2,11,22,33,44).change(hour: 5) - assert_equal Time.local(2005,1,2,11, 6, 0, 0), Time.local(2005,1,2,11,22,33,44).change(min: 6) - assert_equal Time.local(2005,1,2,11,22, 7, 0), Time.local(2005,1,2,11,22,33,44).change(sec: 7) - assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(usec: 8) - assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(nsec: 8000) - assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(usec: 1, nsec: 1) } + assert_equal Time.local(2006, 2, 22, 15, 15, 10), Time.local(2005, 2, 22, 15, 15, 10).change(year: 2006) + assert_equal Time.local(2005, 6, 22, 15, 15, 10), Time.local(2005, 2, 22, 15, 15, 10).change(month: 6) + assert_equal Time.local(2012, 9, 22, 15, 15, 10), Time.local(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9) + assert_equal Time.local(2005, 2, 22, 16), Time.local(2005, 2, 22, 15, 15, 10).change(hour: 16) + assert_equal Time.local(2005, 2, 22, 16, 45), Time.local(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45) + assert_equal Time.local(2005, 2, 22, 15, 45), Time.local(2005, 2, 22, 15, 15, 10).change(min: 45) + + assert_equal Time.local(2005, 1, 2, 5, 0, 0, 0), Time.local(2005, 1, 2, 11, 22, 33, 44).change(hour: 5) + assert_equal Time.local(2005, 1, 2, 11, 6, 0, 0), Time.local(2005, 1, 2, 11, 22, 33, 44).change(min: 6) + assert_equal Time.local(2005, 1, 2, 11, 22, 7, 0), Time.local(2005, 1, 2, 11, 22, 33, 44).change(sec: 7) + assert_equal Time.local(2005, 1, 2, 11, 22, 33, 8), Time.local(2005, 1, 2, 11, 22, 33, 44).change(usec: 8) + assert_equal Time.local(2005, 1, 2, 11, 22, 33, 8), Time.local(2005, 1, 2, 11, 22, 33, 2).change(nsec: 8000) + assert_raise(ArgumentError) { Time.local(2005, 1, 2, 11, 22, 33, 8).change(usec: 1, nsec: 1) } assert_nothing_raised { Time.new(2015, 5, 9, 10, 00, 00, "+03:00").change(nsec: 999999999) } end def test_utc_change - assert_equal Time.utc(2006,2,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(year: 2006) - assert_equal Time.utc(2005,6,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(month: 6) - assert_equal Time.utc(2012,9,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(year: 2012, month: 9) - assert_equal Time.utc(2005,2,22,16), Time.utc(2005,2,22,15,15,10).change(hour: 16) - assert_equal Time.utc(2005,2,22,16,45), Time.utc(2005,2,22,15,15,10).change(hour: 16, min: 45) - assert_equal Time.utc(2005,2,22,15,45), Time.utc(2005,2,22,15,15,10).change(min: 45) - assert_equal Time.utc(2005,1,2,11,22,33,8), Time.utc(2005,1,2,11,22,33,2).change(nsec: 8000) + assert_equal Time.utc(2006, 2, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).change(year: 2006) + assert_equal Time.utc(2005, 6, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).change(month: 6) + assert_equal Time.utc(2012, 9, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9) + assert_equal Time.utc(2005, 2, 22, 16), Time.utc(2005, 2, 22, 15, 15, 10).change(hour: 16) + assert_equal Time.utc(2005, 2, 22, 16, 45), Time.utc(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45) + assert_equal Time.utc(2005, 2, 22, 15, 45), Time.utc(2005, 2, 22, 15, 15, 10).change(min: 45) + assert_equal Time.utc(2005, 1, 2, 11, 22, 33, 8), Time.utc(2005, 1, 2, 11, 22, 33, 2).change(nsec: 8000) end def test_offset_change - assert_equal Time.new(2006,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(year: 2006) - assert_equal Time.new(2005,6,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(month: 6) - assert_equal Time.new(2012,9,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(year: 2012, month: 9) - assert_equal Time.new(2005,2,22,16,0,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(hour: 16) - assert_equal Time.new(2005,2,22,16,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(hour: 16, min: 45) - assert_equal Time.new(2005,2,22,15,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(min: 45) - assert_equal Time.new(2005,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,0,"-08:00").change(sec: 10) - assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(usec: 10).usec - assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(nsec: 10).nsec + assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(year: 2006) + assert_equal Time.new(2005, 6, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(month: 6) + assert_equal Time.new(2012, 9, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(year: 2012, month: 9) + assert_equal Time.new(2005, 2, 22, 16, 0, 0, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(hour: 16) + assert_equal Time.new(2005, 2, 22, 16, 45, 0, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(hour: 16, min: 45) + assert_equal Time.new(2005, 2, 22, 15, 45, 0, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(min: 45) + assert_equal Time.new(2005, 2, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 0, "-08:00").change(sec: 10) + assert_equal 10, Time.new(2005, 2, 22, 15, 15, 0, "-08:00").change(usec: 10).usec + assert_equal 10, Time.new(2005, 2, 22, 15, 15, 0, "-08:00").change(nsec: 10).nsec assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(usec: 1000000) } assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(nsec: 1000000000) } end + def test_change_offset + assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2006, 2, 22, 15, 15, 10, "+01:00").change(offset: "-08:00") + assert_equal Time.new(2006, 2, 22, 15, 15, 10, -28800), Time.new(2006, 2, 22, 15, 15, 10, 3600).change(offset: -28800) + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(usec: 1000000, offset: "-08:00") } + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(nsec: 1000000000, offset: -28800) } + end + def test_advance - assert_equal Time.local(2006,2,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(years: 1) - assert_equal Time.local(2005,6,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(months: 4) - assert_equal Time.local(2005,3,21,15,15,10), Time.local(2005,2,28,15,15,10).advance(weeks: 3) - assert_equal Time.local(2005,3,25,3,15,10), Time.local(2005,2,28,15,15,10).advance(weeks: 3.5) - assert_in_delta Time.local(2005,3,26,12,51,10), Time.local(2005,2,28,15,15,10).advance(weeks: 3.7), 1 - assert_equal Time.local(2005,3,5,15,15,10), Time.local(2005,2,28,15,15,10).advance(days: 5) - assert_equal Time.local(2005,3,6,3,15,10), Time.local(2005,2,28,15,15,10).advance(days: 5.5) - assert_in_delta Time.local(2005,3,6,8,3,10), Time.local(2005,2,28,15,15,10).advance(days: 5.7), 1 - assert_equal Time.local(2012,9,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(years: 7, months: 7) - 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,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) - assert_equal Time.local(2005,2,28,20,22,19), Time.local(2005,2,28,15,15,10).advance(hours: 5, minutes: 7, seconds: 9) - assert_equal Time.local(2005,2,28,10,8,1), Time.local(2005,2,28,15,15,10).advance(hours: -5, minutes: -7, seconds: -9) - assert_equal Time.local(2013,10,17,20,22,19), Time.local(2005,2,28,15,15,10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) + assert_equal Time.local(2006, 2, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 1) + assert_equal Time.local(2005, 6, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(months: 4) + assert_equal Time.local(2005, 3, 21, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(weeks: 3) + assert_equal Time.local(2005, 3, 25, 3, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(weeks: 3.5) + assert_in_delta Time.local(2005, 3, 26, 12, 51, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(weeks: 3.7), 1 + assert_equal Time.local(2005, 3, 5, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(days: 5) + assert_equal Time.local(2005, 3, 6, 3, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(days: 5.5) + assert_in_delta Time.local(2005, 3, 6, 8, 3, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(days: 5.7), 1 + assert_equal Time.local(2012, 9, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 7) + 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, 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) + assert_equal Time.local(2005, 2, 28, 20, 22, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: 5, minutes: 7, seconds: 9) + assert_equal Time.local(2005, 2, 28, 10, 8, 1), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: -5, minutes: -7, seconds: -9) + assert_equal Time.local(2013, 10, 17, 20, 22, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) end def test_utc_advance - assert_equal Time.utc(2006,2,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(years: 1) - assert_equal Time.utc(2005,6,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(months: 4) - assert_equal Time.utc(2005,3,21,15,15,10), Time.utc(2005,2,28,15,15,10).advance(weeks: 3) - assert_equal Time.utc(2005,3,25,3,15,10), Time.utc(2005,2,28,15,15,10).advance(weeks: 3.5) - assert_in_delta Time.utc(2005,3,26,12,51,10), Time.utc(2005,2,28,15,15,10).advance(weeks: 3.7), 1 - assert_equal Time.utc(2005,3,5,15,15,10), Time.utc(2005,2,28,15,15,10).advance(days: 5) - assert_equal Time.utc(2005,3,6,3,15,10), Time.utc(2005,2,28,15,15,10).advance(days: 5.5) - assert_in_delta Time.utc(2005,3,6,8,3,10), Time.utc(2005,2,28,15,15,10).advance(days: 5.7), 1 - assert_equal Time.utc(2012,9,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(years: 7, months: 7) - 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,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) - assert_equal Time.utc(2005,2,28,20,22,19), Time.utc(2005,2,28,15,15,10).advance(hours: 5, minutes: 7, seconds: 9) - assert_equal Time.utc(2005,2,28,10,8,1), Time.utc(2005,2,28,15,15,10).advance(hours: -5, minutes: -7, seconds: -9) - assert_equal Time.utc(2013,10,17,20,22,19), Time.utc(2005,2,28,15,15,10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) + assert_equal Time.utc(2006, 2, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 1) + assert_equal Time.utc(2005, 6, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(months: 4) + assert_equal Time.utc(2005, 3, 21, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(weeks: 3) + assert_equal Time.utc(2005, 3, 25, 3, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(weeks: 3.5) + assert_in_delta Time.utc(2005, 3, 26, 12, 51, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(weeks: 3.7), 1 + assert_equal Time.utc(2005, 3, 5, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(days: 5) + assert_equal Time.utc(2005, 3, 6, 3, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(days: 5.5) + assert_in_delta Time.utc(2005, 3, 6, 8, 3, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(days: 5.7), 1 + assert_equal Time.utc(2012, 9, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 7, months: 7) + 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, 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) + assert_equal Time.utc(2005, 2, 28, 20, 22, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: 5, minutes: 7, seconds: 9) + assert_equal Time.utc(2005, 2, 28, 10, 8, 1), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: -5, minutes: -7, seconds: -9) + assert_equal Time.utc(2013, 10, 17, 20, 22, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) end def test_offset_advance - assert_equal Time.new(2006,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").advance(years: 1) - assert_equal Time.new(2005,6,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").advance(months: 4) - assert_equal Time.new(2005,3,21,15,15,10,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(weeks: 3) - assert_equal Time.new(2005,3,25,3,15,10,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(weeks: 3.5) - assert_in_delta Time.new(2005,3,26,12,51,10,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(weeks: 3.7), 1 - assert_equal Time.new(2005,3,5,15,15,10,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(days: 5) - assert_equal Time.new(2005,3,6,3,15,10,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(days: 5.5) - assert_in_delta Time.new(2005,3,6,8,3,10,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(days: 5.7), 1 - assert_equal Time.new(2012,9,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").advance(years: 7, months: 7) - 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,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) - assert_equal Time.new(2005,2,28,20,22,19,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(hours: 5, minutes: 7, seconds: 9) - assert_equal Time.new(2005,2,28,10,8,1,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(hours: -5, minutes: -7, seconds: -9) - assert_equal Time.new(2013,10,17,20,22,19,"-08:00"), Time.new(2005,2,28,15,15,10,"-08:00").advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) + assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 1) + assert_equal Time.new(2005, 6, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(months: 4) + assert_equal Time.new(2005, 3, 21, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(weeks: 3) + assert_equal Time.new(2005, 3, 25, 3, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(weeks: 3.5) + assert_in_delta Time.new(2005, 3, 26, 12, 51, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(weeks: 3.7), 1 + assert_equal Time.new(2005, 3, 5, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(days: 5) + assert_equal Time.new(2005, 3, 6, 3, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(days: 5.5) + assert_in_delta Time.new(2005, 3, 6, 8, 3, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(days: 5.7), 1 + assert_equal Time.new(2012, 9, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 7, months: 7) + 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, 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) + assert_equal Time.new(2005, 2, 28, 20, 22, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: 5, minutes: 7, seconds: 9) + assert_equal Time.new(2005, 2, 28, 10, 8, 1, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: -5, minutes: -7, seconds: -9) + assert_equal Time.new(2013, 10, 17, 20, 22, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9) end def test_advance_with_nsec @@ -505,37 +512,37 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_advance_gregorian_proleptic - assert_equal Time.local(1582,10,14,15,15,10), Time.local(1582,10,15,15,15,10).advance(days: -1) - assert_equal Time.local(1582,10,15,15,15,10), Time.local(1582,10,14,15,15,10).advance(days: 1) - assert_equal Time.local(1582,10,5,15,15,10), Time.local(1582,10,4,15,15,10).advance(days: 1) - assert_equal Time.local(1582,10,4,15,15,10), Time.local(1582,10,5,15,15,10).advance(days: -1) + assert_equal Time.local(1582, 10, 14, 15, 15, 10), Time.local(1582, 10, 15, 15, 15, 10).advance(days: -1) + assert_equal Time.local(1582, 10, 15, 15, 15, 10), Time.local(1582, 10, 14, 15, 15, 10).advance(days: 1) + assert_equal Time.local(1582, 10, 5, 15, 15, 10), Time.local(1582, 10, 4, 15, 15, 10).advance(days: 1) + assert_equal Time.local(1582, 10, 4, 15, 15, 10), Time.local(1582, 10, 5, 15, 15, 10).advance(days: -1) end def test_last_week with_env_tz "US/Eastern" do - assert_equal Time.local(2005,2,21), Time.local(2005,3,1,15,15,10).last_week - assert_equal Time.local(2005,2,22), Time.local(2005,3,1,15,15,10).last_week(:tuesday) - assert_equal Time.local(2005,2,25), Time.local(2005,3,1,15,15,10).last_week(:friday) - assert_equal Time.local(2006,10,30), Time.local(2006,11,6,0,0,0).last_week - assert_equal Time.local(2006,11,15), Time.local(2006,11,23,0,0,0).last_week(:wednesday) + assert_equal Time.local(2005, 2, 21), Time.local(2005, 3, 1, 15, 15, 10).last_week + assert_equal Time.local(2005, 2, 22), Time.local(2005, 3, 1, 15, 15, 10).last_week(:tuesday) + assert_equal Time.local(2005, 2, 25), Time.local(2005, 3, 1, 15, 15, 10).last_week(:friday) + assert_equal Time.local(2006, 10, 30), Time.local(2006, 11, 6, 0, 0, 0).last_week + assert_equal Time.local(2006, 11, 15), Time.local(2006, 11, 23, 0, 0, 0).last_week(:wednesday) end end def test_next_week_near_daylight_start with_env_tz "US/Eastern" do - assert_equal Time.local(2006,4,3), Time.local(2006,4,2,23,1,0).next_week, "just crossed standard => daylight" + assert_equal Time.local(2006, 4, 3), Time.local(2006, 4, 2, 23, 1, 0).next_week, "just crossed standard => daylight" end with_env_tz "NZ" do - assert_equal Time.local(2006,10,2), Time.local(2006,10,1,23,1,0).next_week, "just crossed standard => daylight" + assert_equal Time.local(2006, 10, 2), Time.local(2006, 10, 1, 23, 1, 0).next_week, "just crossed standard => daylight" end end def test_next_week_near_daylight_end with_env_tz "US/Eastern" do - assert_equal Time.local(2006,10,30), Time.local(2006,10,29,23,1,0).next_week, "just crossed daylight => standard" + assert_equal Time.local(2006, 10, 30), Time.local(2006, 10, 29, 23, 1, 0).next_week, "just crossed daylight => standard" end with_env_tz "NZ" do - assert_equal Time.local(2006,3,20), Time.local(2006,3,19,23,1,0).next_week, "just crossed daylight => standard" + assert_equal Time.local(2006, 3, 20), Time.local(2006, 3, 19, 23, 1, 0).next_week, "just crossed daylight => standard" end end @@ -569,6 +576,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase Time::DATE_FORMATS.delete(:custom) end + def test_rfc3339_with_fractional_seconds + time = Time.new(1999, 12, 31, 19, 0, Rational(1, 8), -18000) + assert_equal "1999-12-31T19:00:00.125-05:00", time.rfc3339(3) + end + def test_to_date assert_equal Date.new(2005, 2, 21), Time.local(2005, 2, 21, 17, 44, 30).to_date end @@ -660,72 +672,72 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase def test_today_with_time_local Date.stub(:current, Date.new(2000, 1, 1)) do - assert_equal false, Time.local(1999,12,31,23,59,59).today? - assert_equal true, Time.local(2000,1,1,0).today? - assert_equal true, Time.local(2000,1,1,23,59,59).today? - assert_equal false, Time.local(2000,1,2,0).today? + assert_equal false, Time.local(1999, 12, 31, 23, 59, 59).today? + assert_equal true, Time.local(2000, 1, 1, 0).today? + assert_equal true, Time.local(2000, 1, 1, 23, 59, 59).today? + assert_equal false, Time.local(2000, 1, 2, 0).today? end end def test_today_with_time_utc Date.stub(:current, Date.new(2000, 1, 1)) do - assert_equal false, Time.utc(1999,12,31,23,59,59).today? - assert_equal true, Time.utc(2000,1,1,0).today? - assert_equal true, Time.utc(2000,1,1,23,59,59).today? - assert_equal false, Time.utc(2000,1,2,0).today? + assert_equal false, Time.utc(1999, 12, 31, 23, 59, 59).today? + assert_equal true, Time.utc(2000, 1, 1, 0).today? + assert_equal true, Time.utc(2000, 1, 1, 23, 59, 59).today? + assert_equal false, Time.utc(2000, 1, 2, 0).today? end end def test_past_with_time_current_as_time_local with_env_tz "US/Eastern" do - Time.stub(:current, Time.local(2005,2,10,15,30,45)) do - assert_equal true, Time.local(2005,2,10,15,30,44).past? - assert_equal false, Time.local(2005,2,10,15,30,45).past? - assert_equal false, Time.local(2005,2,10,15,30,46).past? - assert_equal true, Time.utc(2005,2,10,20,30,44).past? - assert_equal false, Time.utc(2005,2,10,20,30,45).past? - assert_equal false, Time.utc(2005,2,10,20,30,46).past? + Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do + assert_equal true, Time.local(2005, 2, 10, 15, 30, 44).past? + assert_equal false, Time.local(2005, 2, 10, 15, 30, 45).past? + assert_equal false, Time.local(2005, 2, 10, 15, 30, 46).past? + assert_equal true, Time.utc(2005, 2, 10, 20, 30, 44).past? + assert_equal false, Time.utc(2005, 2, 10, 20, 30, 45).past? + assert_equal false, Time.utc(2005, 2, 10, 20, 30, 46).past? end end end def test_past_with_time_current_as_time_with_zone with_env_tz "US/Eastern" do - twz = Time.utc(2005,2,10,15,30,45).in_time_zone("Central Time (US & Canada)") + twz = Time.utc(2005, 2, 10, 15, 30, 45).in_time_zone("Central Time (US & Canada)") Time.stub(:current, twz) do - assert_equal true, Time.local(2005,2,10,10,30,44).past? - assert_equal false, Time.local(2005,2,10,10,30,45).past? - assert_equal false, Time.local(2005,2,10,10,30,46).past? - assert_equal true, Time.utc(2005,2,10,15,30,44).past? - assert_equal false, Time.utc(2005,2,10,15,30,45).past? - assert_equal false, Time.utc(2005,2,10,15,30,46).past? + assert_equal true, Time.local(2005, 2, 10, 10, 30, 44).past? + assert_equal false, Time.local(2005, 2, 10, 10, 30, 45).past? + assert_equal false, Time.local(2005, 2, 10, 10, 30, 46).past? + assert_equal true, Time.utc(2005, 2, 10, 15, 30, 44).past? + assert_equal false, Time.utc(2005, 2, 10, 15, 30, 45).past? + assert_equal false, Time.utc(2005, 2, 10, 15, 30, 46).past? end end end def test_future_with_time_current_as_time_local with_env_tz "US/Eastern" do - Time.stub(:current, Time.local(2005,2,10,15,30,45)) do - assert_equal false, Time.local(2005,2,10,15,30,44).future? - assert_equal false, Time.local(2005,2,10,15,30,45).future? - assert_equal true, Time.local(2005,2,10,15,30,46).future? - assert_equal false, Time.utc(2005,2,10,20,30,44).future? - assert_equal false, Time.utc(2005,2,10,20,30,45).future? - assert_equal true, Time.utc(2005,2,10,20,30,46).future? + Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do + assert_equal false, Time.local(2005, 2, 10, 15, 30, 44).future? + assert_equal false, Time.local(2005, 2, 10, 15, 30, 45).future? + assert_equal true, Time.local(2005, 2, 10, 15, 30, 46).future? + assert_equal false, Time.utc(2005, 2, 10, 20, 30, 44).future? + assert_equal false, Time.utc(2005, 2, 10, 20, 30, 45).future? + assert_equal true, Time.utc(2005, 2, 10, 20, 30, 46).future? end end end def test_future_with_time_current_as_time_with_zone with_env_tz "US/Eastern" do - twz = Time.utc(2005,2,10,15,30,45).in_time_zone("Central Time (US & Canada)") + twz = Time.utc(2005, 2, 10, 15, 30, 45).in_time_zone("Central Time (US & Canada)") Time.stub(:current, twz) do - assert_equal false, Time.local(2005,2,10,10,30,44).future? - assert_equal false, Time.local(2005,2,10,10,30,45).future? - assert_equal true, Time.local(2005,2,10,10,30,46).future? - assert_equal false, Time.utc(2005,2,10,15,30,44).future? - assert_equal false, Time.utc(2005,2,10,15,30,45).future? - assert_equal true, Time.utc(2005,2,10,15,30,46).future? + assert_equal false, Time.local(2005, 2, 10, 10, 30, 44).future? + assert_equal false, Time.local(2005, 2, 10, 10, 30, 45).future? + assert_equal true, Time.local(2005, 2, 10, 10, 30, 46).future? + assert_equal false, Time.utc(2005, 2, 10, 15, 30, 44).future? + assert_equal false, Time.utc(2005, 2, 10, 15, 30, 45).future? + assert_equal true, Time.utc(2005, 2, 10, 15, 30, 46).future? end end end @@ -762,16 +774,16 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase 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(2000, 1, 1, 0, 0, 1), 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(2000, 1, 1, 0, 0, 1, 0).to_s) - assert_equal nil, Time.utc(2000) <=> "Invalid as Time" + 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 def test_at_with_datetime @@ -851,13 +863,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_eql? - assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"]) ) - assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) - assert_equal false,Time.utc(2000, 1, 1, 0, 0, 1).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"]) ) + assert_equal true, Time.utc(2000).eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"])) + assert_equal true, Time.utc(2000).eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"])) + assert_equal false, Time.utc(2000, 1, 1, 0, 0, 1).eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"])) 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 @@ -883,32 +895,63 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_all_day - assert_equal Time.local(2011,6,7,0,0,0)..Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_day + assert_equal Time.local(2011, 6, 7, 0, 0, 0)..Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_day end def test_all_day_with_timezone - beginning_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011,6,7,0,0,0)) - end_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011,6,7,23,59,59,Rational(999999999, 1000))) + beginning_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011, 6, 7, 0, 0, 0)) + end_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000))) - assert_equal beginning_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011,6,7,10,10,10), ActiveSupport::TimeZone["Hawaii"]).all_day.begin - assert_equal end_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011,6,7,10,10,10), ActiveSupport::TimeZone["Hawaii"]).all_day.end + assert_equal beginning_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011, 6, 7, 10, 10, 10), ActiveSupport::TimeZone["Hawaii"]).all_day.begin + assert_equal end_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011, 6, 7, 10, 10, 10), ActiveSupport::TimeZone["Hawaii"]).all_day.end end def test_all_week - assert_equal Time.local(2011,6,6,0,0,0)..Time.local(2011,6,12,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_week - assert_equal Time.local(2011,6,5,0,0,0)..Time.local(2011,6,11,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_week(:sunday) + assert_equal Time.local(2011, 6, 6, 0, 0, 0)..Time.local(2011, 6, 12, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_week + assert_equal Time.local(2011, 6, 5, 0, 0, 0)..Time.local(2011, 6, 11, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_week(:sunday) end def test_all_month - assert_equal Time.local(2011,6,1,0,0,0)..Time.local(2011,6,30,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_month + assert_equal Time.local(2011, 6, 1, 0, 0, 0)..Time.local(2011, 6, 30, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_month end def test_all_quarter - assert_equal Time.local(2011,4,1,0,0,0)..Time.local(2011,6,30,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_quarter + assert_equal Time.local(2011, 4, 1, 0, 0, 0)..Time.local(2011, 6, 30, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_quarter end def test_all_year - assert_equal Time.local(2011,1,1,0,0,0)..Time.local(2011,12,31,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_year + assert_equal Time.local(2011, 1, 1, 0, 0, 0)..Time.local(2011, 12, 31, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_year + end + + def test_rfc3339_parse + time = Time.rfc3339("1999-12-31T19:00:00.125-05:00") + + assert_equal 1999, time.year + assert_equal 12, time.month + assert_equal 31, time.day + assert_equal 19, time.hour + assert_equal 0, time.min + assert_equal 0, time.sec + assert_equal 125000, time.usec + assert_equal(-18000, time.utc_offset) + + exception = assert_raises(ArgumentError) do + Time.rfc3339("1999-12-31") + end + + assert_equal "invalid date", exception.message + + exception = assert_raises(ArgumentError) do + Time.rfc3339("1999-12-31T19:00:00") + end + + assert_equal "invalid date", exception.message + + exception = assert_raises(ArgumentError) do + Time.rfc3339("foobar") + end + + assert_equal "invalid date", exception.message 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 e35aa6e154..70ae793cda 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -90,7 +90,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_nsec - local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)) + local = Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000)) with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local) assert_equal local.nsec, with_zone.nsec @@ -144,6 +144,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil) end + def test_iso8601_with_fractional_seconds + @twz += Rational(1, 8) + assert_equal "1999-12-31T19:00:00.125-05:00", @twz.iso8601(3) + end + + def test_rfc3339_with_fractional_seconds + @twz += Rational(1, 8) + assert_equal "1999-12-31T19:00:00.125-05:00", @twz.rfc3339(3) + end + def test_to_yaml yaml = <<-EOF.strip_heredoc --- !ruby/object:ActiveSupport::TimeWithZone @@ -215,69 +225,69 @@ class TimeWithZoneTest < ActiveSupport::TestCase 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(2000, 1, 1, 0, 0, 1), 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 def test_between? - assert @twz.between?(Time.utc(1999,12,31,23,59,59), Time.utc(2000,1,1,0,0,1)) - assert_equal false, @twz.between?(Time.utc(2000,1,1,0,0,1), Time.utc(2000,1,1,0,0,2)) + assert @twz.between?(Time.utc(1999, 12, 31, 23, 59, 59), Time.utc(2000, 1, 1, 0, 0, 1)) + assert_equal false, @twz.between?(Time.utc(2000, 1, 1, 0, 0, 1), Time.utc(2000, 1, 1, 0, 0, 2)) end def test_today Date.stub(:current, Date.new(2000, 1, 1)) do - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(1999,12,31,23,59,59) ).today? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,0) ).today? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,23,59,59) ).today? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,2,0) ).today? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(1999, 12, 31, 23, 59, 59)).today? + assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 1, 0)).today? + assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 1, 23, 59, 59)).today? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 2, 0)).today? end end def test_past_with_time_current_as_time_local with_env_tz "US/Eastern" do - Time.stub(:current, Time.local(2005,2,10,15,30,45)) do - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do + assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).past? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).past? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).past? end end end def test_past_with_time_current_as_time_with_zone - twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)) Time.stub(:current, twz) do - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).past? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).past? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).past? end end def test_future_with_time_current_as_time_local with_env_tz "US/Eastern" do - Time.stub(:current, Time.local(2005,2,10,15,30,45)) do - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).future? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).future? + assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).future? end end end def test_future_with_time_current_as_time_with_zone - twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)) Time.stub(:current, twz) do - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).future? + assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).future? + assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).future? end end def test_eql? assert_equal true, @twz.eql?(@twz.dup) assert_equal true, @twz.eql?(Time.utc(2000)) - assert_equal true, @twz.eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) - assert_equal false, @twz.eql?( Time.utc(2000, 1, 1, 0, 0, 1) ) - assert_equal false, @twz.eql?( DateTime.civil(1999, 12, 31, 23, 59, 59) ) + assert_equal true, @twz.eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"])) + assert_equal false, @twz.eql?(Time.utc(2000, 1, 1, 0, 0, 1)) + assert_equal false, @twz.eql?(DateTime.civil(1999, 12, 31, 23, 59, 59)) other_twz = ActiveSupport::TimeWithZone.new(DateTime.now.utc, @time_zone) assert_equal true, other_twz.eql?(other_twz.dup) @@ -289,117 +299,117 @@ 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 twz = ActiveSupport::TimeWithZone.new(Time.utc(2038, 1, 19), @time_zone) - assert_equal [0, 0, 19, 19, 1, 2038], (twz + 86_400).to_a[0,6] + assert_equal [0, 0, 19, 19, 1, 2038], (twz + 86_400).to_a[0, 6] 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 - twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"] ) - twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"] ) - assert_equal 86_400.0, twz2 - twz1 + twz1 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"]) + twz2 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) + assert_equal 86_400.0, twz2 - twz1 end 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"] ) + 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 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 - utc = Time.utc(2006,4,2,6,59,59) # == Apr 2 2006 01:59:59 EST; i.e., 1 second before daylight savings start + utc = Time.utc(2006, 4, 2, 6, 59, 59) # == Apr 2 2006 01:59:59 EST; i.e., 1 second before daylight savings start twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) - assert_equal Time.utc(2006,4,2,1,59,59), twz.time + assert_equal Time.utc(2006, 4, 2, 1, 59, 59), twz.time assert_equal false, twz.dst? assert_equal "EST", twz.zone twz = twz + 1 - assert_equal Time.utc(2006,4,2,3), twz.time # adding 1 sec springs forward to 3:00AM EDT + assert_equal Time.utc(2006, 4, 2, 3), twz.time # adding 1 sec springs forward to 3:00AM EDT assert_equal true, twz.dst? assert_equal "EDT", twz.zone twz = twz - 1 # subtracting 1 second takes goes back to 1:59:59AM EST - assert_equal Time.utc(2006,4,2,1,59,59), twz.time + assert_equal Time.utc(2006, 4, 2, 1, 59, 59), twz.time assert_equal false, twz.dst? assert_equal "EST", twz.zone end def test_plus_and_minus_enforce_fall_dst_rules - utc = Time.utc(2006,10,29,5,59,59) # == Oct 29 2006 01:59:59 EST; i.e., 1 second before daylight savings end + utc = Time.utc(2006, 10, 29, 5, 59, 59) # == Oct 29 2006 01:59:59 EST; i.e., 1 second before daylight savings end twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) - assert_equal Time.utc(2006,10,29,1,59,59), twz.time + assert_equal Time.utc(2006, 10, 29, 1, 59, 59), twz.time assert_equal true, twz.dst? assert_equal "EDT", twz.zone twz = twz + 1 - assert_equal Time.utc(2006,10,29,1), twz.time # adding 1 sec falls back from 1:59:59 EDT to 1:00AM EST + assert_equal Time.utc(2006, 10, 29, 1), twz.time # adding 1 sec falls back from 1:59:59 EDT to 1:00AM EST assert_equal false, twz.dst? assert_equal "EST", twz.zone twz = twz - 1 - assert_equal Time.utc(2006,10,29,1,59,59), twz.time # subtracting 1 sec goes back to 1:59:59AM EDT + assert_equal Time.utc(2006, 10, 29, 1, 59, 59), twz.time # subtracting 1 sec goes back to 1:59:59AM EDT assert_equal true, twz.dst? assert_equal "EDT", twz.zone end def test_to_a - assert_equal [45, 30, 5, 1, 2, 2000, 2, 32, false, "HST"], ActiveSupport::TimeWithZone.new( Time.utc(2000, 2, 1, 15, 30, 45), ActiveSupport::TimeZone["Hawaii"] ).to_a + assert_equal [45, 30, 5, 1, 2, 2000, 2, 32, false, "HST"], ActiveSupport::TimeWithZone.new(Time.utc(2000, 2, 1, 15, 30, 45), ActiveSupport::TimeZone["Hawaii"]).to_a end def test_to_f - result = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"] ).to_f + result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"]).to_f assert_equal 946684800.0, result assert_kind_of Float, result end def test_to_i - result = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"] ).to_i + result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"]).to_i assert_equal 946684800, result assert_kind_of Integer, result end @@ -421,23 +431,41 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal time, Time.at(time) end - def test_to_time - with_env_tz "US/Eastern" do - assert_equal Time, @twz.to_time.class - assert_equal Time.local(1999, 12, 31, 19), @twz.to_time - assert_equal Time.local(1999, 12, 31, 19).utc_offset, @twz.to_time.utc_offset + def test_to_time_with_preserve_timezone + with_preserve_timezone(true) do + with_env_tz "US/Eastern" do + time = @twz.to_time + + assert_equal Time, time.class + assert_equal time.object_id, @twz.to_time.object_id + assert_equal Time.local(1999, 12, 31, 19), time + assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset + end + end + end + + def test_to_time_without_preserve_timezone + with_preserve_timezone(false) do + with_env_tz "US/Eastern" do + time = @twz.to_time + + assert_equal Time, time.class + assert_equal time.object_id, @twz.to_time.object_id + assert_equal Time.local(1999, 12, 31, 19), time + assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset + end end end def test_to_date # 1 sec before midnight Jan 1 EST - assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 4, 59, 59), ActiveSupport::TimeZone["Eastern Time (US & Canada)"] ).to_date + assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 4, 59, 59), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date # midnight Jan 1 EST - assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 5, 0, 0), ActiveSupport::TimeZone["Eastern Time (US & Canada)"] ).to_date + assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 5, 0, 0), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date # 1 sec before midnight Jan 2 EST - assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 4, 59, 59), ActiveSupport::TimeZone["Eastern Time (US & Canada)"] ).to_date + assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 4, 59, 59), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date # midnight Jan 2 EST - assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 5, 0, 0), ActiveSupport::TimeZone["Eastern Time (US & Canada)"] ).to_date + assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 5, 0, 0), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date end def test_to_datetime @@ -455,6 +483,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal false, ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:date) end + def test_blank? + assert_not @twz.blank? + end + def test_is_a assert_kind_of Time, @twz assert_kind_of Time, @twz @@ -467,7 +499,7 @@ 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 @@ -503,6 +535,8 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_nothing_raised do @twz.period @twz.time + @twz.to_datetime + @twz.to_time end end @@ -513,7 +547,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_date_part_value_methods - twz = ActiveSupport::TimeWithZone.new(Time.utc(1999,12,31,19,18,17,500), @time_zone) + twz = ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 19, 18, 17, 500), @time_zone) assert_not_called(twz, :method_missing) do assert_equal 1999, twz.year assert_equal 12, twz.month @@ -533,12 +567,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_usec_returns_sec_fraction_when_datetime_is_wrapped - twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone) + twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)), @time_zone) assert_equal 500000, twz.usec end def test_nsec_returns_sec_fraction_when_datetime_is_wrapped - twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone) + twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)), @time_zone) assert_equal 500000000, twz.nsec end @@ -554,15 +588,15 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_instance_created_with_local_time_enforces_spring_dst_rules - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,2)) # first second of DST - assert_equal Time.utc(2006,4,2,3), twz.time # springs forward to 3AM + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 2)) # first second of DST + assert_equal Time.utc(2006, 4, 2, 3), twz.time # springs forward to 3AM assert_equal true, twz.dst? assert_equal "EDT", twz.zone end def test_instance_created_with_local_time_enforces_fall_dst_rules - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,1)) # 1AM can be either DST or non-DST; we'll pick DST - assert_equal Time.utc(2006,10,29,1), twz.time + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 29, 1)) # 1AM can be either DST or non-DST; we'll pick DST + assert_equal Time.utc(2006, 10, 29, 1), twz.time assert_equal true, twz.dst? assert_equal "EDT", twz.zone end @@ -575,11 +609,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_utc_to_local_conversion_with_far_future_datetime - assert_equal [0,0,19,31,12,2049], ActiveSupport::TimeWithZone.new(DateTime.civil(2050), @time_zone).to_a[0,6] + assert_equal [0, 0, 19, 31, 12, 2049], ActiveSupport::TimeWithZone.new(DateTime.civil(2050), @time_zone).to_a[0, 6] end def test_local_to_utc_conversion_with_far_future_datetime - assert_equal DateTime.civil(2050).to_f, ActiveSupport::TimeWithZone.new(nil, @time_zone, DateTime.civil(2049,12,31,19)).to_f + assert_equal DateTime.civil(2050).to_f, ActiveSupport::TimeWithZone.new(nil, @time_zone, DateTime.civil(2049, 12, 31, 19)).to_f end def test_change @@ -591,6 +625,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 06:00:00 EST -05:00", @twz.change(hour: 6).inspect assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.change(min: 15).inspect assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(sec: 30).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: "-10:00").inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: -36000).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Hawaii").inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -10).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -36000).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Pacific/Honolulu").inspect end def test_change_at_dst_boundary @@ -688,7 +728,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_year_from_leap_day - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2004,2,29)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2004, 2, 29)) assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(years: 1).inspect assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.years_since(1).inspect assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.since(1.year).inspect @@ -697,7 +737,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_from_last_day_of_january - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2005,1,31)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2005, 1, 31)) assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(months: 1).inspect assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.months_since(1).inspect assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.since(1.month).inspect @@ -706,7 +746,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_from_last_day_of_january_during_leap_year - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000,1,31)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 31)) assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.advance(months: 1).inspect assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.months_since(1).inspect assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.since(1.month).inspect @@ -715,7 +755,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_into_spring_dst_gap - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,3,2,2)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 3, 2, 2)) assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(months: 1).inspect assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.months_since(1).inspect assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1.month).inspect @@ -724,7 +764,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_second_into_spring_dst_gap - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,1,59,59)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 1, 59, 59)) assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(seconds: 1).inspect assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1).inspect assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1.second).inspect @@ -735,7 +775,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_across_spring_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long # When we advance 1 day, we want to end up at the same time on the next day assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.advance(days: 1).inspect @@ -748,7 +788,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_across_spring_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 10, 30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long # When we advance back 1 day, we want to end up at the same time on the previous day assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(days: -1).inspect @@ -758,7 +798,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 86400).inspect @@ -779,7 +819,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,11,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 11, 30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 86400).inspect @@ -796,7 +836,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_across_fall_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long # When we advance 1 day, we want to end up at the same time on the next day assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.advance(days: 1).inspect @@ -809,7 +849,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_across_fall_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 29, 10, 30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long # When we advance backwards 1 day, we want to end up at the same time on the previous day assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(days: -1).inspect @@ -819,7 +859,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 86400).inspect @@ -840,7 +880,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,9,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 29, 9, 30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 86400).inspect @@ -857,7 +897,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_week_across_spring_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30)) assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.advance(weeks: 1).inspect assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.weeks_since(1).inspect assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.since(1.week).inspect @@ -866,7 +906,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_week_across_spring_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,8,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 8, 10, 30)) assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(weeks: -1).inspect assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.weeks_ago(1).inspect assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.week).inspect @@ -874,7 +914,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_week_across_fall_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30)) assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.advance(weeks: 1).inspect assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.weeks_since(1).inspect assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.since(1.week).inspect @@ -883,7 +923,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_week_across_fall_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,11,4,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 11, 4, 10, 30)) assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(weeks: -1).inspect assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.weeks_ago(1).inspect assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.week).inspect @@ -891,7 +931,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_across_spring_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30)) assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.advance(months: 1).inspect assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.months_since(1).inspect assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.since(1.month).inspect @@ -900,7 +940,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_across_spring_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,5,1,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 5, 1, 10, 30)) assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(months: -1).inspect assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.months_ago(1).inspect assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.month).inspect @@ -908,7 +948,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_across_fall_dst_transition - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30)) assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.advance(months: 1).inspect assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.months_since(1).inspect assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.since(1.month).inspect @@ -917,7 +957,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_month_across_fall_dst_transition_backwards - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,11,28,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 11, 28, 10, 30)) assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(months: -1).inspect assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.months_ago(1).inspect assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.month).inspect @@ -925,7 +965,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_year - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,2,15,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008, 2, 15, 10, 30)) assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.advance(years: 1).inspect assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.years_since(1).inspect assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.since(1.year).inspect @@ -937,7 +977,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_advance_1_year_during_dst - twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,7,15,10,30)) + twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008, 7, 15, 10, 30)) assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.advance(years: 1).inspect assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.years_since(1).inspect assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.since(1.year).inspect @@ -1057,7 +1097,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase Time.zone = -9.hours assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone Time.zone = nil - assert_equal nil, Time.zone + assert_nil Time.zone end def test_time_zone_getter_and_setter_with_zone_default_set diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb new file mode 100644 index 0000000000..67ef6ef619 --- /dev/null +++ b/activesupport/test/current_attributes_test.rb @@ -0,0 +1,96 @@ +require "abstract_unit" + +class CurrentAttributesTest < ActiveSupport::TestCase + Person = Struct.new(:name, :time_zone) + + class Current < ActiveSupport::CurrentAttributes + attribute :world, :account, :person, :request + delegate :time_zone, to: :person + + resets { Time.zone = "UTC" } + + def account=(account) + super + self.person = "#{account}'s person" + end + + def person=(person) + super + Time.zone = person.try(:time_zone) + end + + def request + "#{super} something" + end + + def intro + "#{person.name}, in #{time_zone}" + end + end + + setup { Current.reset } + + test "read and write attribute" do + Current.world = "world/1" + assert_equal "world/1", Current.world + end + + test "read overwritten attribute method" do + Current.request = "request/1" + assert_equal "request/1 something", Current.request + end + + test "set attribute via overwritten method" do + Current.account = "account/1" + assert_equal "account/1", Current.account + assert_equal "account/1's person", Current.person + end + + test "set auxiliary class via overwritten method" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Time.zone.name + end + + test "resets auxiliary class via callback" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Time.zone.name + + Current.reset + assert_equal "UTC", Time.zone.name + end + + test "set attribute only via scope" do + Current.world = "world/1" + + Current.set(world: "world/2") do + assert_equal "world/2", Current.world + end + + assert_equal "world/1", Current.world + end + + test "set multiple attributes" do + Current.world = "world/1" + Current.account = "account/1" + + Current.set(world: "world/2", account: "account/2") do + assert_equal "world/2", Current.world + assert_equal "account/2", Current.account + end + + assert_equal "world/1", Current.world + assert_equal "account/1", Current.account + end + + test "delegation" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Current.time_zone + assert_equal "Central Time (US & Canada)", Current.instance.time_zone + end + + test "all methods forward to the instance" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "David, in Central Time (US & Canada)", Current.intro + assert_equal "David, in Central Time (US & Canada)", Current.instance.intro + end +end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 54068f5a08..1ea36418ff 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -104,7 +104,7 @@ class DependenciesTest < ActiveSupport::TestCase with_loading "dependencies" do old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true filename = "check_warnings" - expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}") + expanded = File.expand_path("dependencies/#{filename}", __dir__) $check_warnings_load_count = 0 assert_not ActiveSupport::Dependencies.loaded.include?(expanded) @@ -121,7 +121,7 @@ class DependenciesTest < ActiveSupport::TestCase silence_warnings { require_dependency filename } assert_equal 2, $check_warnings_load_count - assert_equal nil, $checked_verbose, "After first load warnings should be left alone." + assert_nil $checked_verbose, "After first load warnings should be left alone." assert_includes ActiveSupport::Dependencies.loaded, expanded ActiveSupport::Dependencies.clear @@ -271,7 +271,8 @@ class DependenciesTest < ActiveSupport::TestCase def test_raising_discards_autoloaded_constants with_autoloading_fixtures do - assert_raises(Exception, "arbitray exception message") { RaisesArbitraryException } + e = assert_raises(Exception) { RaisesArbitraryException } + assert_equal("arbitrary exception message", e.message) assert_not defined?(A) assert_not defined?(RaisesArbitraryException) end @@ -292,7 +293,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_doesnt_break_normal_require - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) with_autoloading_fixtures do @@ -311,7 +312,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_doesnt_break_normal_require_nested - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -331,7 +332,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_true_when_file_not_yet_required - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -344,7 +345,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -358,7 +359,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_false_when_file_already_required - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -378,7 +379,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_load_returns_true_when_file_found - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -397,14 +398,17 @@ class DependenciesTest < ActiveSupport::TestCase end end - def failing_test_access_thru_and_upwards_fails - with_autoloading_fixtures do - assert_not defined?(ModuleFolder) - assert_raise(NameError) { ModuleFolder::Object } - assert_raise(NameError) { ModuleFolder::NestedClass::Object } + # This raises only on 2.5.. (warns on ..2.4) + if RUBY_VERSION > "2.5" + def test_access_thru_and_upwards_fails + with_autoloading_fixtures do + assert_not defined?(ModuleFolder) + assert_raise(NameError) { ModuleFolder::Object } + assert_raise(NameError) { ModuleFolder::NestedClass::Object } + end + ensure + remove_constants(:ModuleFolder) end - ensure - remove_constants(:ModuleFolder) end def test_non_existing_const_raises_name_error_with_fully_qualified_name @@ -434,7 +438,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_loadable_constants_for_path_should_handle_relative_paths fake_root = "dependencies" - relative_root = File.dirname(__FILE__) + "/dependencies" + relative_root = File.expand_path("dependencies", __dir__) ["", "/"].each do |suffix| with_loading fake_root + suffix do assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(relative_root + "/a/b") @@ -459,7 +463,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_loadable_constants_with_load_path_without_trailing_slash - path = File.dirname(__FILE__) + "/autoloading_fixtures/class_folder/inline_class.rb" + path = File.expand_path("autoloading_fixtures/class_folder/inline_class.rb", __dir__) with_loading "autoloading_fixtures/class/" do assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path(path) end @@ -526,8 +530,8 @@ class DependenciesTest < ActiveSupport::TestCase def test_file_search with_loading "dependencies" do root = ActiveSupport::Dependencies.autoload_paths.first - assert_equal nil, ActiveSupport::Dependencies.search_for_file("service_three") - assert_equal nil, ActiveSupport::Dependencies.search_for_file("service_three.rb") + assert_nil ActiveSupport::Dependencies.search_for_file("service_three") + assert_nil ActiveSupport::Dependencies.search_for_file("service_three.rb") assert_equal root + "/service_one.rb", ActiveSupport::Dependencies.search_for_file("service_one") assert_equal root + "/service_one.rb", ActiveSupport::Dependencies.search_for_file("service_one.rb") end @@ -548,7 +552,6 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal autoload + "/conflict.rb", ActiveSupport::Dependencies.search_for_file("conflict") end - end def test_custom_const_missing_should_work @@ -757,14 +760,14 @@ class DependenciesTest < ActiveSupport::TestCase def test_new_contants_in_without_constants assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) {}) - assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k,v| v.empty? } + assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } end def test_new_constants_in_with_a_single_constant assert_equal ["Hello"], ActiveSupport::Dependencies.new_constants_in(Object) { Object.const_set :Hello, 10 }.map(&:to_s) - assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k,v| v.empty? } + assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } ensure remove_constants(:Hello) end @@ -781,7 +784,7 @@ class DependenciesTest < ActiveSupport::TestCase end assert_equal ["OuterAfter", "OuterBefore"], outer.sort.map(&:to_s) - assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k,v| v.empty? } + assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } ensure remove_constants(:OuterBefore, :Inner, :OuterAfter) end @@ -800,7 +803,7 @@ class DependenciesTest < ActiveSupport::TestCase M.const_set :OuterAfter, 30 end assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort - assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k,v| v.empty? } + assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } ensure remove_constants(:M) end @@ -818,7 +821,7 @@ class DependenciesTest < ActiveSupport::TestCase M.const_set :OuterAfter, 30 end assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort - assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k,v| v.empty? } + assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } ensure remove_constants(:M) end @@ -988,7 +991,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_remove_constant_does_not_trigger_loading_autoloads constant = "ShouldNotBeAutoloaded" Object.class_eval do - autoload constant, File.expand_path("../autoloading_fixtures/should_not_be_required", __FILE__) + autoload constant, File.expand_path("autoloading_fixtures/should_not_be_required", __dir__) end assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant" diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 9bc63ed89e..451195a143 100644 --- a/activesupport/test/dependencies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -1,7 +1,7 @@ module DependenciesTestHelpers def with_loading(*from) old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load - this_dir = File.dirname(__FILE__) + this_dir = __dir__ parent_dir = File.dirname(this_dir) path_copy = $LOAD_PATH.dup $LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir) diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index e2a3331cfa..257cb50fb2 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -16,7 +16,7 @@ class Deprecatee def not() 2 end def none() 1 end def one(a) a end - def multi(a,b,c) [a,b,c] end + def multi(a, b, c) [a, b, c] end deprecate :none, :one, :multi def a; end @@ -35,6 +35,18 @@ class Deprecatee A = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Deprecatee::A", "Deprecatee::B::C") end +class DeprecateeWithAccessor + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + + module B + C = 1 + end + deprecate_constant "A", "DeprecateeWithAccessor::B::C" + + class NewException < StandardError; end + deprecate_constant "OldException", "DeprecateeWithAccessor::NewException" +end + class DeprecationTest < ActiveSupport::TestCase include ActiveSupport::Testing::Stream @@ -73,7 +85,7 @@ class DeprecationTest < ActiveSupport::TestCase end assert_deprecated(/multi is deprecated/) do - assert_equal [1,2,3], @dtc.multi(1,2,3) + assert_equal [1, 2, 3], @dtc.multi(1, 2, 3) end end @@ -88,16 +100,18 @@ class DeprecationTest < ActiveSupport::TestCase end def test_several_behaviors - @a, @b = nil, nil + @a, @b, @c = nil, nil, nil ActiveSupport::Deprecation.behavior = [ - Proc.new { |msg, callstack| @a = msg }, - Proc.new { |msg, callstack| @b = msg } + lambda { |msg, callstack, horizon, gem| @a = msg }, + lambda { |msg, callstack| @b = msg }, + lambda { |*args| @c = args }, ] @dtc.partially assert_match(/foo=nil/, @a) assert_match(/foo=nil/, @b) + assert_equal 4, @c.size end def test_raise_behaviour @@ -107,7 +121,7 @@ class DeprecationTest < ActiveSupport::TestCase callstack = caller_locations e = assert_raise ActiveSupport::DeprecationException do - ActiveSupport::Deprecation.behavior.first.call(message, callstack) + ActiveSupport::Deprecation.behavior.first.call(message, callstack, "horizon", "gem") end assert_equal message, e.message assert_equal callstack.map(&:to_s), e.backtrace.map(&:to_s) @@ -118,7 +132,7 @@ class DeprecationTest < ActiveSupport::TestCase behavior = ActiveSupport::Deprecation.behavior.first content = capture(:stderr) { - assert_nil behavior.call("Some error!", ["call stack!"]) + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } assert_match(/Some error!/, content) assert_match(/call stack!/, content) @@ -140,11 +154,32 @@ class DeprecationTest < ActiveSupport::TestCase behavior = ActiveSupport::Deprecation.behavior.first stderr_output = capture(:stderr) { - assert_nil behavior.call("Some error!", ["call stack!"]) + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } assert stderr_output.empty? end + def test_default_notify_behavior + ActiveSupport::Deprecation.behavior = :notify + behavior = ActiveSupport::Deprecation.behavior.first + + begin + events = [] + ActiveSupport::Notifications.subscribe("deprecation.my_gem_custom") { |_, **args| + events << args + } + + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "MyGem::Custom") + assert_equal 1, events.size + assert_equal "Some error!", events.first[:message] + assert_equal ["call stack!"], events.first[:callstack] + assert_equal "horizon", events.first[:deprecation_horizon] + assert_equal "MyGem::Custom", events.first[:gem_name] + ensure + ActiveSupport::Notifications.unsubscribe("deprecation.my_gem_custom") + end + end + def test_deprecated_instance_variable_proxy assert_not_deprecated { @dtc.request.size } @@ -162,6 +197,17 @@ class DeprecationTest < ActiveSupport::TestCase assert_not_deprecated { assert_equal Deprecatee::B::C.class, Deprecatee::A.class } end + def test_deprecated_constant_accessor + assert_not_deprecated { DeprecateeWithAccessor::B::C } + assert_deprecated("DeprecateeWithAccessor::A") { assert_equal DeprecateeWithAccessor::B::C, DeprecateeWithAccessor::A } + end + + def test_deprecated_constant_accessor_exception + raise DeprecateeWithAccessor::NewException.new("Test") + rescue DeprecateeWithAccessor::OldException => e + assert_kind_of DeprecateeWithAccessor::NewException, e + end + def test_assert_deprecated_raises_when_method_not_deprecated assert_raises(Minitest::Assertion) { assert_deprecated { @dtc.not } } end @@ -279,12 +325,22 @@ class DeprecationTest < ActiveSupport::TestCase def test_deprecated_constant_with_deprecator_given deprecator = deprecator_with_messages klass = Class.new - klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new("klass::OLD", "Object", deprecator) ) + klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new("klass::OLD", "Object", deprecator)) assert_difference("deprecator.messages.size") do klass::OLD.to_s end end + def test_deprecated_constant_with_custom_message + deprecator = deprecator_with_messages + + klass = Class.new + klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new("klass::OLD", "Object", deprecator, message: "foo")) + + klass::OLD.to_s + assert_match "foo", deprecator.messages.last + end + def test_deprecated_instance_variable_with_instance_deprecator deprecator = deprecator_with_messages diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb index 09c5ce1f07..cf349d53ee 100644 --- a/activesupport/test/descendants_tracker_test_cases.rb +++ b/activesupport/test/descendants_tracker_test_cases.rb @@ -40,7 +40,7 @@ module DescendantsTrackerTestCases end end - protected + private def assert_equal_sets(expected, actual) assert_equal Set.new(expected), Set.new(actual) diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index cc47318974..f33a5f5764 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -7,6 +7,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase def setup skip if ENV["LISTEN"] == "0" + require "listen" super end @@ -30,18 +31,15 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase wait # wait for the events to fire end - def rm_f(files) - super - wait - end - test "notifies forked processes" do + jruby_skip "Forking not available on JRuby" + FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) {} assert !checker.updated? - # Pipes used for flow controll across fork. + # Pipes used for flow control across fork. boot_reader, boot_writer = IO.pipe touch_reader, touch_writer = IO.pipe diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb index 0b56ea008f..7fefc066b3 100644 --- a/activesupport/test/executor_test.rb +++ b/activesupport/test/executor_test.rb @@ -105,7 +105,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.wrap {} - assert_equal nil, supplied_state + assert_nil supplied_state end def test_exception_skips_uninvoked_hook @@ -158,6 +158,63 @@ class ExecutorTest < ActiveSupport::TestCase assert_equal :some_state, supplied_state end + def test_hook_insertion_order + invoked = [] + supplied_state = [] + + hook_class = Class.new do + attr_accessor :letter + + define_method(:initialize) do |letter| + self.letter = letter + end + + define_method(:run) do + invoked << :"run_#{letter}" + :"state_#{letter}" + end + + define_method(:complete) do |state| + invoked << :"complete_#{letter}" + supplied_state << state + end + end + + executor.register_hook(hook_class.new(:a)) + executor.register_hook(hook_class.new(:b)) + executor.register_hook(hook_class.new(:c), outer: true) + executor.register_hook(hook_class.new(:d)) + + executor.wrap {} + + assert_equal [:run_c, :run_a, :run_b, :run_d, :complete_a, :complete_b, :complete_d, :complete_c], invoked + assert_equal [:state_a, :state_b, :state_d, :state_c], supplied_state + end + + def test_class_serial_is_unaffected + skip if !defined?(RubyVM) + + hook = Class.new do + define_method(:run) do + nil + end + + define_method(:complete) do |state| + nil + end + end.new + + executor.register_hook(hook) + + before = RubyVM.stat(:class_serial) + executor.wrap {} + executor.wrap {} + executor.wrap {} + after = RubyVM.stat(:class_serial) + + assert_equal before, after + end + def test_separate_classes_can_wrap other_executor = Class.new(ActiveSupport::Executor) diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb index cd6a58e840..361e7e2349 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -49,6 +49,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } touch(tmpfiles) + wait assert checker.execute_if_updated assert_equal 1, i @@ -62,6 +63,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } touch(tmpfiles) + wait assert checker.execute_if_updated assert_equal 1, i @@ -75,6 +77,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } rm_f(tmpfiles) + wait assert checker.execute_if_updated assert_equal 1, i @@ -87,6 +90,7 @@ module FileUpdateCheckerSharedTests assert !checker.updated? touch(tmpfiles) + wait assert checker.updated? end @@ -100,6 +104,7 @@ module FileUpdateCheckerSharedTests assert !checker.updated? touch(tmpfiles) + wait assert checker.updated? end @@ -113,6 +118,7 @@ module FileUpdateCheckerSharedTests assert !checker.updated? rm_f(tmpfiles) + wait assert checker.updated? end @@ -129,6 +135,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } touch(tmpfiles[1..-1]) + wait assert checker.execute_if_updated assert_equal 1, i @@ -145,6 +152,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } touch(tmpfiles[1..-1]) + wait assert checker.execute_if_updated assert_equal 1, i @@ -157,6 +165,7 @@ module FileUpdateCheckerSharedTests assert !checker.updated? touch(tmpfiles) + wait assert checker.updated? checker.execute @@ -169,6 +178,7 @@ module FileUpdateCheckerSharedTests checker = new_checker([], tmpdir => :rb) { i += 1 } touch(tmpfile("foo.rb")) + wait assert checker.execute_if_updated assert_equal 1, i @@ -180,11 +190,13 @@ module FileUpdateCheckerSharedTests checker = new_checker([], tmpdir => [:rb, :txt]) { i += 1 } touch(tmpfile("foo.rb")) + wait assert checker.execute_if_updated assert_equal 1, i touch(tmpfile("foo.txt")) + wait assert checker.execute_if_updated assert_equal 2, i @@ -196,6 +208,7 @@ module FileUpdateCheckerSharedTests checker = new_checker([], tmpdir => :txt) { i += 1 } touch(tmpfile("foo.rb")) + wait assert !checker.execute_if_updated assert_equal 0, i @@ -208,6 +221,7 @@ module FileUpdateCheckerSharedTests checker = new_checker([non_existing]) { i += 1 } touch(non_existing) + wait assert checker.execute_if_updated assert_equal 1, i @@ -226,6 +240,7 @@ module FileUpdateCheckerSharedTests assert_equal 0, i touch(File.join(subdir, "nested.rb")) + wait assert checker.execute_if_updated assert_equal 1, i @@ -240,19 +255,28 @@ module FileUpdateCheckerSharedTests checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 } touch(tmpfile("new.txt")) + wait assert !checker.execute_if_updated assert_equal 0, i # subdir does not look for Ruby files, but its parent tmpdir does. touch(File.join(subdir, "nested.rb")) + wait assert checker.execute_if_updated assert_equal 1, i touch(File.join(subdir, "nested.txt")) + wait assert checker.execute_if_updated assert_equal 2, i end + + test "initialize raises an ArgumentError if no block given" do + assert_raise ArgumentError do + new_checker([]) + end + end end diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb index d88408b55c..33e0cd2a04 100644 --- a/activesupport/test/gzip_test.rb +++ b/activesupport/test/gzip_test.rb @@ -20,7 +20,7 @@ class GzipTest < ActiveSupport::TestCase end def test_compress_should_return_gzipped_string_by_compression_level - source_string = "Hello World"*100 + source_string = "Hello World" * 100 gzipped_by_speed = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_SPEED) assert_equal 1, Zlib::GzipReader.new(StringIO.new(gzipped_by_speed)).level @@ -30,4 +30,14 @@ class GzipTest < ActiveSupport::TestCase assert_equal true, (gzipped_by_best_compression.bytesize < gzipped_by_speed.bytesize) end + + def test_decompress_checks_crc + compressed = ActiveSupport::Gzip.compress("Hello World") + first_crc_byte_index = compressed.bytesize - 8 + compressed.setbyte(first_crc_byte_index, compressed.getbyte(first_crc_byte_index) ^ 0xff) + + assert_raises(Zlib::GzipFile::CRCError) do + ActiveSupport::Gzip.decompress(compressed) + end + end end diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb new file mode 100644 index 0000000000..d68add46cd --- /dev/null +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -0,0 +1,745 @@ +require "abstract_unit" +require "active_support/core_ext/hash" +require "bigdecimal" +require "active_support/core_ext/string/access" +require "active_support/ordered_hash" +require "active_support/core_ext/object/conversions" +require "active_support/core_ext/object/deep_dup" +require "active_support/inflections" + +class HashWithIndifferentAccessTest < ActiveSupport::TestCase + HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess + + class IndifferentHash < ActiveSupport::HashWithIndifferentAccess + end + + class SubclassingArray < Array + end + + class SubclassingHash < Hash + end + + class NonIndifferentHash < Hash + def nested_under_indifferent_access + self + end + end + + class HashByConversion + def initialize(hash) + @hash = hash + end + + def to_hash + @hash + end + end + + def setup + @strings = { "a" => 1, "b" => 2 } + @nested_strings = { "a" => { "b" => { "c" => 3 } } } + @symbols = { a: 1, b: 2 } + @nested_symbols = { a: { b: { c: 3 } } } + @mixed = { :a => 1, "b" => 2 } + @nested_mixed = { "a" => { b: { "c" => 3 } } } + @integers = { 0 => 1, 1 => 2 } + @nested_integers = { 0 => { 1 => { 2 => 3 } } } + @illegal_symbols = { [] => 3 } + @nested_illegal_symbols = { [] => { [] => 3 } } + end + + def test_symbolize_keys_for_hash_with_indifferent_access + assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys + assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys + assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys + assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys + end + + def test_deep_symbolize_keys_for_hash_with_indifferent_access + assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys + end + + def test_symbolize_keys_bang_for_hash_with_indifferent_access + assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! } + assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! } + assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! } + end + + def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access + assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } + assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! } + assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + + def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access + assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! } + end + + def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access + assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + + def test_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access + assert_equal @integers, @integers.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @integers.with_indifferent_access.dup.symbolize_keys! } + end + + def test_deep_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access + assert_equal @nested_integers, @nested_integers.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_integers.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + + def test_stringify_keys_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys + assert_equal @strings, @symbols.with_indifferent_access.stringify_keys + assert_equal @strings, @strings.with_indifferent_access.stringify_keys + assert_equal @strings, @mixed.with_indifferent_access.stringify_keys + end + + def test_deep_stringify_keys_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys + end + + def test_stringify_keys_bang_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! + end + + def test_deep_stringify_keys_bang_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys! + end + + def test_nested_under_indifferent_access + foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"] + + foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of NonIndifferentHash, foo["foo"] + + foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of IndifferentHash, foo["foo"] + end + + def test_indifferent_assorted + @strings = @strings.with_indifferent_access + @symbols = @symbols.with_indifferent_access + @mixed = @mixed.with_indifferent_access + + assert_equal "a", @strings.__send__(:convert_key, :a) + + assert_equal 1, @strings.fetch("a") + assert_equal 1, @strings.fetch(:a.to_s) + assert_equal 1, @strings.fetch(:a) + + hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed } + method_map = { '[]': 1, fetch: 1, values_at: [1], + has_key?: true, include?: true, key?: true, + member?: true } + + hashes.each do |name, hash| + method_map.sort_by(&:to_s).each do |meth, expected| + assert_equal(expected, hash.__send__(meth, "a"), + "Calling #{name}.#{meth} 'a'") + assert_equal(expected, hash.__send__(meth, :a), + "Calling #{name}.#{meth} :a") + end + end + + assert_equal [1, 2], @strings.values_at("a", "b") + assert_equal [1, 2], @strings.values_at(:a, :b) + assert_equal [1, 2], @symbols.values_at("a", "b") + assert_equal [1, 2], @symbols.values_at(:a, :b) + assert_equal [1, 2], @mixed.values_at("a", "b") + assert_equal [1, 2], @mixed.values_at(:a, :b) + end + + def test_indifferent_fetch_values + skip unless Hash.method_defined?(:fetch_values) + + @mixed = @mixed.with_indifferent_access + + assert_equal [1, 2], @mixed.fetch_values("a", "b") + assert_equal [1, 2], @mixed.fetch_values(:a, :b) + assert_equal [1, 2], @mixed.fetch_values(:a, "b") + assert_equal [1, "c"], @mixed.fetch_values(:a, :c) { |key| key } + assert_raise(KeyError) { @mixed.fetch_values(:a, :c) } + end + + def test_indifferent_reading + hash = HashWithIndifferentAccess.new + hash["a"] = 1 + hash["b"] = true + hash["c"] = false + hash["d"] = nil + + assert_equal 1, hash[:a] + assert_equal true, hash[:b] + assert_equal false, hash[:c] + assert_nil hash[:d] + assert_nil hash[:e] + end + + def test_indifferent_reading_with_nonnil_default + hash = HashWithIndifferentAccess.new(1) + hash["a"] = 1 + hash["b"] = true + hash["c"] = false + hash["d"] = nil + + assert_equal 1, hash[:a] + assert_equal true, hash[:b] + assert_equal false, hash[:c] + assert_nil hash[:d] + assert_equal 1, hash[:e] + end + + def test_indifferent_writing + hash = HashWithIndifferentAccess.new + hash[:a] = 1 + hash["b"] = 2 + hash[3] = 3 + + assert_equal 1, hash["a"] + assert_equal 2, hash["b"] + assert_equal 1, hash[:a] + assert_equal 2, hash[:b] + assert_equal 3, hash[3] + end + + def test_indifferent_update + hash = HashWithIndifferentAccess.new + hash[:a] = "a" + hash["b"] = "b" + + updated_with_strings = hash.update(@strings) + updated_with_symbols = hash.update(@symbols) + updated_with_mixed = hash.update(@mixed) + + assert_equal 1, updated_with_strings[:a] + assert_equal 1, updated_with_strings["a"] + assert_equal 2, updated_with_strings["b"] + + assert_equal 1, updated_with_symbols[:a] + assert_equal 2, updated_with_symbols["b"] + assert_equal 2, updated_with_symbols[:b] + + assert_equal 1, updated_with_mixed[:a] + assert_equal 2, updated_with_mixed["b"] + + assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 } + end + + def test_update_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + hash.update HashByConversion.new(a: 1) + assert_equal 1, hash["a"] + end + + def test_indifferent_merging + hash = HashWithIndifferentAccess.new + hash[:a] = "failure" + hash["b"] = "failure" + + other = { "a" => 1, :b => 2 } + + merged = hash.merge(other) + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 1, merged[:a] + assert_equal 2, merged["b"] + + hash.update(other) + + assert_equal 1, hash[:a] + assert_equal 2, hash["b"] + end + + def test_merge_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + merged = hash.merge HashByConversion.new(a: 1) + assert_equal 1, merged["a"] + end + + def test_indifferent_replace + hash = HashWithIndifferentAccess.new + hash[:a] = 42 + + replaced = hash.replace(b: 12) + + assert hash.key?("b") + assert !hash.key?(:a) + assert_equal 12, hash[:b] + assert_same hash, replaced + end + + def test_replace_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + hash[:a] = 42 + + replaced = hash.replace(HashByConversion.new(b: 12)) + + assert hash.key?("b") + assert !hash.key?(:a) + assert_equal 12, hash[:b] + assert_same hash, replaced + end + + def test_indifferent_merging_with_block + hash = HashWithIndifferentAccess.new + hash[:a] = 1 + hash["b"] = 3 + + other = { "a" => 4, :b => 2, "c" => 10 } + + merged = hash.merge(other) { |key, old, new| old > new ? old : new } + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 4, merged[:a] + assert_equal 3, merged["b"] + assert_equal 10, merged[:c] + + other_indifferent = HashWithIndifferentAccess.new("a" => 9, :b => 2) + + merged = hash.merge(other_indifferent) { |key, old, new| old + new } + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 10, merged[:a] + assert_equal 5, merged[:b] + end + + def test_indifferent_reverse_merging + hash = HashWithIndifferentAccess.new key: :old_value + hash.reverse_merge! key: :new_value + assert_equal :old_value, hash[:key] + + hash = HashWithIndifferentAccess.new("some" => "value", "other" => "value") + hash.reverse_merge!(some: "noclobber", another: "clobber") + assert_equal "value", hash[:some] + assert_equal "clobber", hash[:another] + end + + def test_indifferent_with_defaults_aliases_reverse_merge + hash = HashWithIndifferentAccess.new key: :old_value + actual = hash.with_defaults key: :new_value + assert_equal :old_value, actual[:key] + + hash = HashWithIndifferentAccess.new key: :old_value + hash.with_defaults! key: :new_value + assert_equal :old_value, hash[:key] + end + + def test_indifferent_deleting + get_hash = proc { { a: "foo" }.with_indifferent_access } + hash = get_hash.call + assert_equal "foo", hash.delete(:a) + assert_nil hash.delete(:a) + hash = get_hash.call + assert_equal "foo", hash.delete("a") + assert_nil hash.delete("a") + end + + def test_indifferent_select + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| v == 1 } + + assert_equal({ "a" => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_select_returns_enumerator + enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).select + assert_instance_of Enumerator, enum + end + + def test_indifferent_select_returns_a_hash_when_unchanged + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| true } + + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_select_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.select! { |k, v| v == 1 } + + assert_equal({ "a" => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + + def test_indifferent_reject + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject { |k, v| v != 1 } + + assert_equal({ "a" => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_reject_returns_enumerator + enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject + assert_instance_of Enumerator, enum + end + + def test_indifferent_reject_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.reject! { |k, v| v != 1 } + + assert_equal({ "a" => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + + def test_indifferent_compact + hash_contain_nil_value = @strings.merge("z" => nil) + hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) + compacted_hash = hash.compact + + assert_equal(@strings, compacted_hash) + assert_equal(hash_contain_nil_value, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, compacted_hash + + empty_hash = ActiveSupport::HashWithIndifferentAccess.new + compacted_hash = empty_hash.compact + + assert_equal compacted_hash, empty_hash + + non_empty_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: :bar) + compacted_hash = non_empty_hash.compact + + assert_equal compacted_hash, non_empty_hash + end + + def test_indifferent_to_hash + # Should convert to a Hash with String keys. + assert_equal @strings, @mixed.with_indifferent_access.to_hash + + # Should preserve the default value. + mixed_with_default = @mixed.dup + mixed_with_default.default = "1234" + roundtrip = mixed_with_default.with_indifferent_access.to_hash + assert_equal @strings, roundtrip + assert_equal "1234", roundtrip.default + + # Ensure nested hashes are not HashWithIndiffereneAccess + new_to_hash = @nested_mixed.with_indifferent_access.to_hash + assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) + assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) + assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess) + end + + def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access + hash = HashWithIndifferentAccess.new { |h, k| h[k] = [] } + hash[:a] << 1 + + assert_equal [1], hash[:a] + end + + def test_with_indifferent_access_has_no_side_effects_on_existing_hash + hash = { content: [{ :foo => :bar, "bar" => "baz" }] } + hash.with_indifferent_access + + assert_equal [:foo, "bar"], hash[:content].first.keys + end + + def test_indifferent_hash_with_array_of_hashes + hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] } }.with_indifferent_access + assert_equal "1", hash[:urls][:url].first[:address] + + hash = hash.to_hash + assert_not hash.instance_of?(HashWithIndifferentAccess) + assert_not hash["urls"].instance_of?(HashWithIndifferentAccess) + assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess) + end + + def test_should_preserve_array_subclass_when_value_is_array + array = SubclassingArray.new + array << { "address" => "1" } + hash = { "urls" => { "url" => array } }.with_indifferent_access + assert_equal SubclassingArray, hash[:urls][:url].class + end + + def test_should_preserve_array_class_when_hash_value_is_frozen_array + array = SubclassingArray.new + array << { "address" => "1" } + hash = { "urls" => { "url" => array.freeze } }.with_indifferent_access + assert_equal SubclassingArray, hash[:urls][:url].class + end + + def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h[:first] = 1 + h = h.stringify_keys + assert_equal 1, h["first"] + h = HashWithIndifferentAccess.new + h["first"] = 1 + h = h.symbolize_keys + assert_equal 1, h[:first] + end + + def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h[:first] = 1 + h = h.deep_stringify_keys + assert_equal 1, h["first"] + h = HashWithIndifferentAccess.new + h["first"] = 1 + h = h.deep_symbolize_keys + assert_equal 1, h[:first] + end + + def test_to_options_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h["first"] = 1 + h.to_options! + assert_equal 1, h["first"] + end + + def test_to_options_on_indifferent_preserves_works_as_hash_with_dup + h = HashWithIndifferentAccess.new(a: { b: "b" }) + dup = h.dup + + dup[:a][:c] = "c" + assert_equal "c", h[:a][:c] + end + + def test_indifferent_sub_hashes + h = { "user" => { "id" => 5 } }.with_indifferent_access + ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } + + h = { user: { id: 5 } }.with_indifferent_access + ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } + end + + def test_indifferent_duplication + # Should preserve default value + h = HashWithIndifferentAccess.new + h.default = "1234" + assert_equal h.default, h.dup.default + + # Should preserve class for subclasses + h = IndifferentHash.new + assert_equal h.class, h.dup.class + end + + def test_nested_dig_indifferent_access + skip if RUBY_VERSION < "2.3.0" + data = { "this" => { "views" => 1234 } }.with_indifferent_access + assert_equal 1234, data.dig(:this, :views) + end + + def test_assorted_keys_not_stringified + original = { Object.new => 2, 1 => 2, [] => true } + indiff = original.with_indifferent_access + assert(!indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!") + end + + def test_deep_merge_on_indifferent_access + hash_1 = HashWithIndifferentAccess.new(a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } }) + hash_2 = HashWithIndifferentAccess.new(a: 1, c: { c1: 2, c3: { d2: "d2" } }) + hash_3 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } + expected = { "a" => 1, "b" => "b", "c" => { "c1" => 2, "c2" => "c2", "c3" => { "d1" => "d1", "d2" => "d2" } } } + assert_equal expected, hash_1.deep_merge(hash_2) + assert_equal expected, hash_1.deep_merge(hash_3) + + hash_1.deep_merge!(hash_2) + assert_equal expected, hash_1 + end + + def test_store_on_indifferent_access + hash = HashWithIndifferentAccess.new + hash.store(:test1, 1) + hash.store("test1", 11) + hash[:test2] = 2 + hash["test2"] = 22 + expected = { "test1" => 11, "test2" => 22 } + assert_equal expected, hash + end + + def test_constructor_on_indifferent_access + hash = HashWithIndifferentAccess[:foo, 1] + assert_equal 1, hash[:foo] + assert_equal 1, hash["foo"] + hash[:foo] = 3 + assert_equal 3, hash[:foo] + assert_equal 3, hash["foo"] + end + + def test_indifferent_slice + original = { a: "x", b: "y", c: 10 }.with_indifferent_access + expected = { a: "x", b: "y" }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + # Should return a new hash with only the given keys. + assert_equal expected, original.slice(*keys), keys.inspect + assert_not_equal expected, original + end + end + + def test_indifferent_slice_inplace + original = { a: "x", b: "y", c: 10 }.with_indifferent_access + expected = { c: 10 }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + # Should replace the hash with only the given keys. + copy = original.dup + assert_equal expected, copy.slice!(*keys) + end + end + + def test_indifferent_slice_access_with_symbols + original = { "login" => "bender", "password" => "shiny", "stuff" => "foo" } + original = original.with_indifferent_access + + slice = original.slice(:login, :password) + + assert_equal "bender", slice[:login] + assert_equal "bender", slice["login"] + end + + def test_indifferent_extract + original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access + expected = { a: 1, b: 2 }.with_indifferent_access + remaining = { c: 3, d: 4 }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + copy = original.dup + assert_equal expected, copy.extract!(*keys) + assert_equal remaining, copy + end + end + + def test_new_with_to_hash_conversion + hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1)) + assert hash.key?("a") + assert_equal 1, hash[:a] + end + + def test_dup_with_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, v| raise "walrus" } + assert_nothing_raised { hash.dup } + end + + def test_dup_with_default_proc_sets_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| k + 1 } + new_hash = hash.dup + + assert_equal 3, new_hash[2] + + new_hash.default = 2 + assert_equal 2, new_hash[:non_existent] + end + + def test_to_hash_with_raising_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| raise "walrus" } + + assert_nothing_raised { hash.to_hash } + end + + def test_new_with_to_hash_conversion_copies_default + normal_hash = Hash.new(3) + normal_hash[:a] = 1 + + hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) + assert_equal 1, hash[:a] + assert_equal 3, hash[:b] + end + + def test_new_with_to_hash_conversion_copies_default_proc + normal_hash = Hash.new { 1 + 2 } + normal_hash[:a] = 1 + + hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) + assert_equal 1, hash[:a] + assert_equal 3, hash[:b] + end + + def test_inheriting_from_top_level_hash_with_indifferent_access_preserves_ancestors_chain + klass = Class.new(::HashWithIndifferentAccess) + assert_equal ActiveSupport::HashWithIndifferentAccess, klass.ancestors[1] + end + + def test_inheriting_from_hash_with_indifferent_access_properly_dumps_ivars + klass = Class.new(::HashWithIndifferentAccess) do + def initialize(*) + @foo = "bar" + super + end + end + + yaml_output = klass.new.to_yaml + + # `hash-with-ivars` was introduced in 2.0.9 (https://git.io/vyUQW) + if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("2.0.9") + assert_includes yaml_output, "hash-with-ivars" + assert_includes yaml_output, "@foo: bar" + else + assert_includes yaml_output, "hash" + end + end + + def test_should_use_default_proc_for_unknown_key + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_equal 3, hash_wia[:new_key] + end + + def test_should_return_nil_if_no_key_is_supplied + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_nil hash_wia.default + end + + def test_should_use_default_value_for_unknown_key + hash_wia = HashWithIndifferentAccess.new(3) + assert_equal 3, hash_wia[:new_key] + end + + def test_should_use_default_value_if_no_key_is_supplied + hash_wia = HashWithIndifferentAccess.new(3) + assert_equal 3, hash_wia.default + end + + def test_should_nil_if_no_default_value_is_supplied + hash_wia = HashWithIndifferentAccess.new + assert_nil hash_wia.default + end + + def test_should_return_dup_for_with_indifferent_access + hash_wia = HashWithIndifferentAccess.new + assert_equal hash_wia, hash_wia.with_indifferent_access + assert_not_same hash_wia, hash_wia.with_indifferent_access + end + + def test_allows_setting_frozen_array_values_with_indifferent_access + value = [1, 2, 3].freeze + hash = HashWithIndifferentAccess.new + hash[:key] = value + assert_equal hash[:key], value + end + + def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access + hash = Hash.new(3) + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia.default + end + + def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access + hash = Hash.new do + 2 + 1 + end + assert_equal 3, hash[:foo] + + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia[:foo] + assert_equal 3, hash_wia[:bar] + end +end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 1c5a4f378c..ef956eda90 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -31,6 +31,32 @@ class InflectorTest < ActiveSupport::TestCase assert_equal "", ActiveSupport::Inflector.pluralize("") end + test "uncountability of ascii word" do + word = "HTTP" + ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable word + end + + assert_equal word, ActiveSupport::Inflector.pluralize(word) + assert_equal word, ActiveSupport::Inflector.singularize(word) + assert_equal ActiveSupport::Inflector.pluralize(word), ActiveSupport::Inflector.singularize(word) + + ActiveSupport::Inflector.inflections.uncountables.pop + end + + test "uncountability of non-ascii word" do + word = "猫" + ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable word + end + + assert_equal word, ActiveSupport::Inflector.pluralize(word) + assert_equal word, ActiveSupport::Inflector.singularize(word) + assert_equal ActiveSupport::Inflector.pluralize(word), ActiveSupport::Inflector.singularize(word) + + ActiveSupport::Inflector.inflections.uncountables.pop + end + ActiveSupport::Inflector.inflections.uncountable.each do |word| define_method "test_uncountability_of_#{word}" do assert_equal word, ActiveSupport::Inflector.singularize(word) @@ -93,6 +119,13 @@ class InflectorTest < ActiveSupport::TestCase end end + MixtureToTitleCaseWithKeepIdSuffix.each_with_index do |(before, titleized), index| + define_method "test_titleize_with_keep_id_suffix_mixture_to_title_case_#{index}" do + assert_equal(titleized, ActiveSupport::Inflector.titleize(before, keep_id_suffix: true), + "mixture to TitleCase with keep_id_suffix failed for #{before}") + end + end + def test_camelize CamelToUnderscore.each do |camel, underscore| assert_equal(camel, ActiveSupport::Inflector.camelize(underscore)) @@ -245,58 +278,30 @@ class InflectorTest < ActiveSupport::TestCase end end - # FIXME: get following tests to pass on jruby, currently skipped - # - # Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes - # required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby - # causing our tests to error out. - # related bug http://jira.codehaus.org/browse/JRUBY-7194 def test_parameterize - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) end end def test_parameterize_and_normalize - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizedAndNormalized.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) end end def test_parameterize_with_custom_separator - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: "_")) end end - def test_parameterize_with_custom_separator_deprecated - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" - StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| - assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do - assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, "_")) - end - end - end - def test_parameterize_with_multi_character_separator - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| assert_equal(parameterized_string.gsub("-", "__sep__"), ActiveSupport::Inflector.parameterize(some_string, separator: "__sep__")) end end - def test_parameterize_with_multi_character_separator_deprecated - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" - StringToParameterized.each do |some_string, parameterized_string| - assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '__sep__'` instead./i) do - assert_equal(parameterized_string.gsub("-", "__sep__"), ActiveSupport::Inflector.parameterize(some_string, "__sep__")) - end - end - end - def test_classify ClassNameToTableName.each do |class_name, table_name| assert_equal(class_name, ActiveSupport::Inflector.classify(table_name)) @@ -326,6 +331,12 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_humanize_with_keep_id_suffix + UnderscoreToHumanWithKeepIdSuffix.each do |underscore, human| + assert_equal(human, ActiveSupport::Inflector.humanize(underscore, keep_id_suffix: true)) + end + end + def test_humanize_by_rule ActiveSupport::Inflector.inflections do |inflect| inflect.human(/_cnt$/i, '\1_count') @@ -409,6 +420,8 @@ class InflectorTest < ActiveSupport::TestCase inflect.singular(/es$/, "") inflect.irregular("el", "los") + + inflect.uncountable("agua") end assert_equal("hijos", "hijo".pluralize(:es)) @@ -421,12 +434,17 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("los", "el".pluralize(:es)) assert_equal("els", "el".pluralize) + assert_equal("agua", "agua".pluralize(:es)) + assert_equal("aguas", "agua".pluralize) + 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? end def test_clear_all @@ -525,12 +543,4 @@ class InflectorTest < ActiveSupport::TestCase end end end - - def test_inflections_with_uncountable_words - ActiveSupport::Inflector.inflections do |inflect| - inflect.uncountable "HTTP" - end - - assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP") - end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index a8283301b3..d61ca3fc18 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -237,7 +237,7 @@ module InflectorTestCases "Ops\331" => "opsu", "Ærøskøbing" => "aeroskobing", "Aßlar" => "asslar", - "Japanese: 日本語" => "japanese" + "Japanese: 日本語" => "japanese" } UnderscoreToHuman = { @@ -248,12 +248,27 @@ module InflectorTestCases "_external_id" => "External" } + UnderscoreToHumanWithKeepIdSuffix = { + "this_is_a_string_ending_with_id" => "This is a string ending with id", + "employee_id" => "Employee id", + "employee_id_something_else" => "Employee id something else", + "underground" => "Underground", + "_id" => "Id", + "_external_id" => "External id" + } + UnderscoreToHumanWithoutCapitalize = { "employee_salary" => "employee salary", "employee_id" => "employee", "underground" => "underground" } + MixtureToTitleCaseWithKeepIdSuffix = { + "this_is_a_string_ending_with_id" => "This Is A String Ending With Id", + "EmployeeId" => "Employee Id", + "Author Id" => "Author Id" + } + MixtureToTitleCase = { "active_record" => "Active Record", "ActiveRecord" => "Active Record", @@ -271,6 +286,7 @@ module InflectorTestCases "¿por qué?" => "¿Por Qué?", "Fred’s" => "Fred’s", "Fred`s" => "Fred`s", + "this was 'fake news'" => "This Was 'Fake News'", ActiveSupport::SafeBuffer.new("confirmation num") => "Confirmation Num" } diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index dd8382754b..6f5051c312 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -15,7 +15,7 @@ class TestJSONDecoding < ActiveSupport::TestCase TESTS = { %q({"returnTo":{"\/categories":"\/"}}) => { "returnTo" => { "/categories" => "/" } }, %q({"return\\"To\\":":{"\/categories":"\/"}}) => { "return\"To\":" => { "/categories" => "/" } }, - %q({"returnTo":{"\/categories":1}}) => { "returnTo" => { "/categories" => 1 } }, + %q({"returnTo":{"\/categories":1}}) => { "returnTo" => { "/categories" => 1 } }, %({"returnTo":[1,"a"]}) => { "returnTo" => [1, "a"] }, %({"returnTo":[1,"\\"a\\",", "b"]}) => { "returnTo" => [1, "\"a\",", "b"] }, %({"a": "'", "b": "5,000"}) => { "a" => "'", "b" => "5,000" }, @@ -40,8 +40,8 @@ class TestJSONDecoding < ActiveSupport::TestCase %({"a": "2007-01-01 : it's your birthday"}) => { "a" => "2007-01-01 : it's your birthday" }, %([]) => [], %({}) => {}, - %({"a":1}) => { "a" => 1 }, - %({"a": ""}) => { "a" => "" }, + %({"a":1}) => { "a" => 1 }, + %({"a": ""}) => { "a" => "" }, %({"a":"\\""}) => { "a" => "\"" }, %({"a": null}) => { "a" => nil }, %({"a": true}) => { "a" => true }, @@ -51,19 +51,19 @@ class TestJSONDecoding < ActiveSupport::TestCase %q({"a": "\u003cunicode\u0020escape\u003e"}) => { "a" => "<unicode escape>" }, '{"a": "\\\\u0020skip double backslashes"}' => { "a" => "\\u0020skip double backslashes" }, %q({"a": "\u003cbr /\u003e"}) => { "a" => "<br />" }, - %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => { "b" => ["<i>","<b>","<u>"] }, + %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => { "b" => ["<i>", "<b>", "<u>"] }, # test combination of dates and escaped or unicode encoded data in arrays %q([{"d":"1970-01-01", "s":"\u0020escape"},{"d":"1970-01-01", "s":"\u0020escape"}]) => - [{ "d" => Date.new(1970, 1, 1), "s" => " escape" },{ "d" => Date.new(1970, 1, 1), "s" => " escape" }], + [{ "d" => Date.new(1970, 1, 1), "s" => " escape" }, { "d" => Date.new(1970, 1, 1), "s" => " escape" }], %q([{"d":"1970-01-01","s":"http:\/\/example.com"},{"d":"1970-01-01","s":"http:\/\/example.com"}]) => [{ "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }, { "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }], # tests escaping of "\n" char with Yaml backend - %q({"a":"\n"}) => { "a"=>"\n" }, - %q({"a":"\u000a"}) => { "a"=>"\n" }, - %q({"a":"Line1\u000aLine2"}) => { "a"=>"Line1\nLine2" }, + %q({"a":"\n"}) => { "a" => "\n" }, + %q({"a":"\u000a"}) => { "a" => "\n" }, + %q({"a":"Line1\u000aLine2"}) => { "a" => "Line1\nLine2" }, # prevent json unmarshalling - '{"json_class":"TestJSONDecoding::Foo"}' => { "json_class"=>"TestJSONDecoding::Foo" }, + '{"json_class":"TestJSONDecoding::Foo"}' => { "json_class" => "TestJSONDecoding::Foo" }, # json "fragments" - these are invalid JSON, but ActionPack relies on this '"a string"' => "a string", "1.1" => 1.1, @@ -75,12 +75,17 @@ class TestJSONDecoding < ActiveSupport::TestCase } TESTS.each_with_index do |(json, expected), index| + fail_message = "JSON decoding failed for #{json}" + test "json decodes #{index}" do with_tz_default "Eastern Time (US & Canada)" do with_parse_json_times(true) do silence_warnings do - assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ - failed for #{json}" + if expected.nil? + assert_nil ActiveSupport::JSON.decode(json), fail_message + else + assert_equal expected, ActiveSupport::JSON.decode(json), fail_message + end end end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index d125cc939f..6d8f7cfbd0 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -49,7 +49,7 @@ class TestJSONEncoding < ActiveSupport::TestCase def test_hash_encoding assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(a: :b) assert_equal %({\"a\":1}), ActiveSupport::JSON.encode("a" => 1) - assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode("a" => [1,2]) + assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode("a" => [1, 2]) assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2) assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(a: :b, c: :d)) @@ -108,7 +108,7 @@ class TestJSONEncoding < ActiveSupport::TestCase def test_time_to_json_includes_local_offset with_standard_json_time_format(true) do with_env_tz "US/Eastern" do - assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) + assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005, 2, 1, 15, 15, 10)) end end end @@ -135,7 +135,7 @@ class TestJSONEncoding < ActiveSupport::TestCase h = JSONTest::Hashlike.new json = h.to_json only: [:foo] - assert_equal({ "foo"=>"hello" }, JSON.parse(json)) + assert_equal({ "foo" => "hello" }, JSON.parse(json)) end def test_object_to_json_with_options @@ -144,7 +144,7 @@ class TestJSONEncoding < ActiveSupport::TestCase obj.instance_variable_set :@bar, "world" json = obj.to_json only: ["foo"] - assert_equal({ "foo"=>"hello" }, JSON.parse(json)) + assert_equal({ "foo" => "hello" }, JSON.parse(json)) end def test_struct_to_json_with_options @@ -153,7 +153,7 @@ class TestJSONEncoding < ActiveSupport::TestCase struct.bar = "world" json = struct.to_json only: [:foo] - assert_equal({ "foo"=>"hello" }, JSON.parse(json)) + assert_equal({ "foo" => "hello" }, JSON.parse(json)) end def test_hash_should_pass_encoding_options_to_children_in_as_json @@ -256,7 +256,7 @@ class TestJSONEncoding < ActiveSupport::TestCase class CustomWithOptions attr_accessor :foo, :bar - def as_json(options={}) + def as_json(options = {}) options[:only] = %w(foo bar) super(options) end @@ -268,8 +268,8 @@ class TestJSONEncoding < ActiveSupport::TestCase f.bar = "world" hash = { "foo" => f, "other_hash" => { "foo" => "other_foo", "test" => "other_test" } } - assert_equal({ "foo"=>{ "foo"=>"hello","bar"=>"world" }, - "other_hash" => { "foo"=>"other_foo","test"=>"other_test" } }, ActiveSupport::JSON.decode(hash.to_json)) + assert_equal({ "foo" => { "foo" => "hello", "bar" => "world" }, + "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }, ActiveSupport::JSON.decode(hash.to_json)) end def test_array_to_json_should_not_keep_options_around @@ -278,8 +278,8 @@ class TestJSONEncoding < ActiveSupport::TestCase f.bar = "world" array = [f, { "foo" => "other_foo", "test" => "other_test" }] - assert_equal([{ "foo"=>"hello","bar"=>"world" }, - { "foo"=>"other_foo","test"=>"other_test" }], ActiveSupport::JSON.decode(array.to_json)) + assert_equal([{ "foo" => "hello", "bar" => "world" }, + { "foo" => "other_foo", "test" => "other_test" }], ActiveSupport::JSON.decode(array.to_json)) end class OptionsTest @@ -329,7 +329,7 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_nil_true_and_false_represented_as_themselves - assert_equal nil, nil.as_json + assert_nil nil.as_json assert_equal true, true.as_json assert_equal false, false.as_json end @@ -341,7 +341,7 @@ class TestJSONEncoding < ActiveSupport::TestCase super end - def as_json(options={}) + def as_json(options = {}) @as_json_called = true super end @@ -432,7 +432,27 @@ EXPECTED assert_equal '"foo"', ActiveSupport::JSON.encode(exception) end - protected + class InfiniteNumber + def as_json(options = nil) + { "number" => Float::INFINITY } + end + end + + def test_to_json_works_when_as_json_returns_infinite_number + assert_equal '{"number":null}', InfiniteNumber.new.to_json + end + + class NaNNumber + def as_json(options = nil) + { "number" => Float::NAN } + end + end + + def test_to_json_works_when_as_json_returns_NaN_number + assert_equal '{"number":null}', NaNNumber.new.to_json + end + + private def object_keys(json_object) json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort diff --git a/activesupport/test/json/encoding_test_cases.rb b/activesupport/test/json/encoding_test_cases.rb index ff2ed3a788..7e4775cec8 100644 --- a/activesupport/test/json/encoding_test_cases.rb +++ b/activesupport/test/json/encoding_test_cases.rb @@ -1,4 +1,8 @@ require "bigdecimal" +require "date" +require "time" +require "pathname" +require "uri" module JSONTest class Foo @@ -23,7 +27,7 @@ module JSONTest end end - class MyStruct < Struct.new(:name, :value) + MyStruct = Struct.new(:name, :value) do def initialize(*) @unused = "unused instance variable" super @@ -36,10 +40,10 @@ module JSONTest NilTests = [[ nil, %(null) ]] NumericTests = [[ 1, %(1) ], [ 2.5, %(2.5) ], - [ 0.0/0.0, %(null) ], - [ 1.0/0.0, %(null) ], - [ -1.0/0.0, %(null) ], - [ BigDecimal("0.0")/BigDecimal("0.0"), %(null) ], + [ 0.0 / 0.0, %(null) ], + [ 1.0 / 0.0, %(null) ], + [ -1.0 / 0.0, %(null) ], + [ BigDecimal("0.0") / BigDecimal("0.0"), %(null) ], [ BigDecimal("2.5"), %("#{BigDecimal('2.5')}") ]] StringTests = [[ "this is the <string>", %("this is the \\u003cstring\\u003e")], @@ -80,13 +84,13 @@ module JSONTest PathnameTests = [[ Pathname.new("lib/index.rb"), %("lib/index.rb") ]] - DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]] - TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] - DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] + DateTests = [[ Date.new(2005, 2, 1), %("2005/02/01") ]] + TimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]] + DateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]] - StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]] - StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]] - StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]] + StandardDateTests = [[ Date.new(2005, 2, 1), %("2005-02-01") ]] + StandardTimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000Z") ]] + StandardDateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000+00:00") ]] StandardStringTests = [[ "this is the <string>", %("this is the <string>")]] end end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index f8282c89ca..4c3515b5e1 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -71,7 +71,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase def test_message_obeys_strict_encoding bad_encoding_characters = "\n!@#" - message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string"+bad_encoding_characters) + message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string" + bad_encoding_characters) assert_not_decrypted("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}") assert_not_verified("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}") @@ -86,20 +86,32 @@ class MessageEncryptorTest < ActiveSupport::TestCase assert_equal @data, encryptor.decrypt_and_verify(message) end + def test_aead_mode_with_hmac_cbc_cipher_text + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + + assert_aead_not_decrypted(encryptor, "eHdGeExnZEwvMSt3U3dKaFl1WFo0TjVvYzA0eGpjbm5WSkt5MXlsNzhpZ0ZnbWhBWFlQZTRwaXE1bVJCS2oxMDZhYVp2dVN3V0lNZUlWQ3c2eVhQbnhnVjFmeVVubmhRKzF3WnZyWHVNMDg9LS1HSisyakJVSFlPb05ISzRMaXRzcFdBPT0=--831a1d54a3cda8a0658dc668a03dedcbce13b5ca") + end + def test_messing_with_aead_values_causes_failures encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") text, iv, auth_tag = encryptor.encrypt_and_sign(@data).split("--") - assert_not_decrypted([iv, text, auth_tag] * "--") - assert_not_decrypted([munge(text), iv, auth_tag] * "--") - assert_not_decrypted([text, munge(iv), auth_tag] * "--") - assert_not_decrypted([text, iv, munge(auth_tag)] * "--") - assert_not_decrypted([munge(text), munge(iv), munge(auth_tag)] * "--") - assert_not_decrypted([text, iv] * "--") - assert_not_decrypted([text, iv, auth_tag[0..-2]] * "--") + assert_aead_not_decrypted(encryptor, [iv, text, auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [munge(text), iv, auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [text, munge(iv), auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [text, iv, munge(auth_tag)] * "--") + assert_aead_not_decrypted(encryptor, [munge(text), munge(iv), munge(auth_tag)] * "--") + assert_aead_not_decrypted(encryptor, [text, iv] * "--") + assert_aead_not_decrypted(encryptor, [text, iv, auth_tag[0..-2]] * "--") end private + def assert_aead_not_decrypted(encryptor, value) + assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do + encryptor.decrypt_and_verify(value) + end + end + def assert_not_decrypted(value) assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do @encryptor.decrypt_and_verify(@verifier.generate(value)) diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index d56a46b250..d6109c761d 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -80,6 +80,6 @@ class MessageVerifierTest < ActiveSupport::TestCase exception = assert_raise(ArgumentError) do ActiveSupport::MessageVerifier.new(nil) end - assert_equal exception.message, "Secret should not be nil." + assert_equal "Secret should not be nil.", exception.message end end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 5ff1543328..d80d340986 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -50,7 +50,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length end - def test_forwarded_method_with_non_string_result_should_be_returned_vertabim + def test_forwarded_method_with_non_string_result_should_be_returned_verbatim str = "" str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @@ -231,7 +231,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert_equal 0, @chars.index("こに") assert_equal 2, @chars.index("ち") assert_equal 2, @chars.index("ち", -2) - assert_equal nil, @chars.index("ち", -1) + assert_nil @chars.index("ち", -1) assert_equal 3, @chars.index("わ") assert_equal 5, "ééxééx".mb_chars.index("x", 4) end @@ -390,11 +390,11 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_slice_should_take_character_offsets - assert_equal nil, "".mb_chars.slice(0) + assert_nil "".mb_chars.slice(0) assert_equal "こ", @chars.slice(0) assert_equal "わ", @chars.slice(3) - assert_equal nil, "".mb_chars.slice(-1..1) - assert_equal nil, "".mb_chars.slice(-1, 1) + assert_nil "".mb_chars.slice(-1..1) + assert_nil "".mb_chars.slice(-1, 1) assert_equal "", "".mb_chars.slice(0..10) assert_equal "にちわ", @chars.slice(1..3) assert_equal "にちわ", @chars.slice(1, 3) @@ -403,10 +403,10 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert_equal "", @chars.slice(4..10) assert_equal "に", @chars.slice(/に/u) assert_equal "にち", @chars.slice(/に./u) - assert_equal nil, @chars.slice(/unknown/u) + assert_nil @chars.slice(/unknown/u) assert_equal "にち", @chars.slice(/(にち)/u, 1) - assert_equal nil, @chars.slice(/(にち)/u, 2) - assert_equal nil, @chars.slice(7..6) + assert_nil @chars.slice(/(にち)/u, 2) + assert_nil @chars.slice(7..6) end def test_slice_bang_returns_sliced_out_substring @@ -414,12 +414,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_slice_bang_returns_nil_on_out_of_bound_arguments - assert_equal nil, @chars.mb_chars.slice!(9..10) + assert_nil @chars.mb_chars.slice!(9..10) end def test_slice_bang_removes_the_slice_from_the_receiver chars = "úüù".mb_chars - chars.slice!(0,2) + chars.slice!(0, 2) assert_equal "ù", chars end @@ -512,7 +512,7 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase { "аБвг аБвг" => "Абвг абвг", "аБвг АБВГ" => "Абвг абвг", "АБВГ АБВГ" => "Абвг абвг", - "" => "" }.each do |f,t| + "" => "" }.each do |f, t| assert_equal t, chars(f).capitalize end end @@ -600,10 +600,10 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase ].pack("U*") assert_equal_codepoints "", chars("").normalize - assert_equal_codepoints [44,105,106,328,323].pack("U*"), chars(comp_str).normalize(:kc).to_s - assert_equal_codepoints [44,307,328,323].pack("U*"), chars(comp_str).normalize(:c).to_s - assert_equal_codepoints [44,307,110,780,78,769].pack("U*"), chars(comp_str).normalize(:d).to_s - assert_equal_codepoints [44,105,106,110,780,78,769].pack("U*"), chars(comp_str).normalize(:kd).to_s + assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s + assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s + assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s + assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s end def test_should_compute_grapheme_length @@ -664,7 +664,6 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase end def test_tidy_bytes_should_tidy_bytes - single_byte_cases = { "\x21" => "!", # Valid ASCII byte, low "\x41" => "A", # Valid ASCII byte, mid diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index bf004d7924..ef1a26135f 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -82,7 +82,7 @@ class MultibyteConformanceTest < ActiveSupport::TestCase end end - protected + private def each_line_of_norm_tests(&block) File.open(File.join(CACHE_DIR, UNIDATA_FILE), "r") do | f | until f.eof? diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb index 04a7d290d9..b3328987ae 100644 --- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -28,7 +28,7 @@ class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase end end - protected + private def each_line_of_break_tests(&block) lines = 0 max_test_lines = 0 # Don't limit below 21, because that's the header of the testfile diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb index e013bd578f..ebc9f92d23 100644 --- a/activesupport/test/multibyte_normalization_conformance_test.rb +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -83,7 +83,7 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase end end - protected + private def each_line_of_norm_tests(&block) lines = 0 max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 22c586c50d..a70516bb08 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -18,7 +18,7 @@ module MultibyteTestHelpers end UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" - CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance" + CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}" FileUtils.mkdir_p(CACHE_DIR) UNICODE_STRING = "こにちわ".freeze @@ -33,7 +33,7 @@ module MultibyteTestHelpers str.to_s.unpack("U*").map { |cp| cp.to_s(16) }.join(" ") end - def assert_equal_codepoints(expected, actual, message=nil) + def assert_equal_codepoints(expected, actual, message = nil) assert_equal(inspect_codepoints(expected), inspect_codepoints(actual), message) end end diff --git a/activesupport/test/notifications/evented_notification_test.rb b/activesupport/test/notifications/evented_notification_test.rb index 688971c858..24c5befec3 100644 --- a/activesupport/test/notifications/evented_notification_test.rb +++ b/activesupport/test/notifications/evented_notification_test.rb @@ -7,7 +7,7 @@ module ActiveSupport attr_reader :events def initialize - @events = [] + @events = [] end def start(name, id, payload) diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb index e454e6897b..7eacc5cbe7 100644 --- a/activesupport/test/notifications/instrumenter_test.rb +++ b/activesupport/test/notifications/instrumenter_test.rb @@ -22,11 +22,11 @@ module ActiveSupport super @notifier = TestNotifier.new @instrumenter = Instrumenter.new @notifier - @payload = { foo: Object.new } + @payload = { foo: Object.new } end def test_instrument - called = false + called = false instrumenter.instrument("foo", payload) { called = true } diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index a6f0d82e8a..11f743519f 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -273,7 +273,7 @@ module Notifications assert !not_child.parent_of?(parent) end - protected + private def random_id @random_id ||= SecureRandom.hex(10) end diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb index 4f58e6607a..a1d1c41dc2 100644 --- a/activesupport/test/number_helper_i18n_test.rb +++ b/activesupport/test/number_helper_i18n_test.rb @@ -1,5 +1,6 @@ require "abstract_unit" require "active_support/number_helper" +require "active_support/core_ext/hash/keys" module ActiveSupport class NumberHelperI18nTest < ActiveSupport::TestCase diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 1a59210018..4caf1428ea 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -150,7 +150,7 @@ module ActiveSupport assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), precision: 20)) assert_equal("111.23460000000000000000", number_helper.number_to_rounded("111.2346", precision: 20)) assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), precision: 20)) - assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded("111.2346", precision: 100)) + assert_equal("111.2346" + "0" * 96, number_helper.number_to_rounded("111.2346", precision: 100)) assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), precision: 4)) assert_equal("0.00", number_helper.number_to_rounded(Rational(0, 1), precision: 2)) end @@ -166,42 +166,42 @@ module ActiveSupport def test_to_rounded_with_significant_digits [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| assert_equal "124000", number_helper.number_to_rounded(123987, precision: 3, significant: true) - assert_equal "120000000", number_helper.number_to_rounded(123987876, precision: 2, significant: true ) - assert_equal "40000", number_helper.number_to_rounded("43523", precision: 1, significant: true ) - assert_equal "9775", number_helper.number_to_rounded(9775, precision: 4, significant: true ) - assert_equal "5.4", number_helper.number_to_rounded(5.3923, precision: 2, significant: true ) - assert_equal "5", number_helper.number_to_rounded(5.3923, precision: 1, significant: true ) - assert_equal "1", number_helper.number_to_rounded(1.232, precision: 1, significant: true ) - assert_equal "7", number_helper.number_to_rounded(7, precision: 1, significant: true ) - assert_equal "1", number_helper.number_to_rounded(1, precision: 1, significant: true ) - assert_equal "53", number_helper.number_to_rounded(52.7923, precision: 2, significant: true ) - assert_equal "9775.00", number_helper.number_to_rounded(9775, precision: 6, significant: true ) - assert_equal "5.392900", number_helper.number_to_rounded(5.3929, precision: 7, significant: true ) - assert_equal "0.0", number_helper.number_to_rounded(0, precision: 2, significant: true ) - assert_equal "0", number_helper.number_to_rounded(0, precision: 1, significant: true ) - assert_equal "0.0001", number_helper.number_to_rounded(0.0001, precision: 1, significant: true ) - assert_equal "0.000100", number_helper.number_to_rounded(0.0001, precision: 3, significant: true ) - assert_equal "0.0001", number_helper.number_to_rounded(0.0001111, precision: 1, significant: true ) + assert_equal "120000000", number_helper.number_to_rounded(123987876, precision: 2, significant: true) + assert_equal "40000", number_helper.number_to_rounded("43523", precision: 1, significant: true) + assert_equal "9775", number_helper.number_to_rounded(9775, precision: 4, significant: true) + assert_equal "5.4", number_helper.number_to_rounded(5.3923, precision: 2, significant: true) + assert_equal "5", number_helper.number_to_rounded(5.3923, precision: 1, significant: true) + assert_equal "1", number_helper.number_to_rounded(1.232, precision: 1, significant: true) + assert_equal "7", number_helper.number_to_rounded(7, precision: 1, significant: true) + assert_equal "1", number_helper.number_to_rounded(1, precision: 1, significant: true) + assert_equal "53", number_helper.number_to_rounded(52.7923, precision: 2, significant: true) + assert_equal "9775.00", number_helper.number_to_rounded(9775, precision: 6, significant: true) + assert_equal "5.392900", number_helper.number_to_rounded(5.3929, precision: 7, significant: true) + assert_equal "0.0", number_helper.number_to_rounded(0, precision: 2, significant: true) + assert_equal "0", number_helper.number_to_rounded(0, precision: 1, significant: true) + assert_equal "0.0001", number_helper.number_to_rounded(0.0001, precision: 1, significant: true) + assert_equal "0.000100", number_helper.number_to_rounded(0.0001, precision: 3, significant: true) + assert_equal "0.0001", number_helper.number_to_rounded(0.0001111, precision: 1, significant: true) assert_equal "10.0", number_helper.number_to_rounded(9.995, precision: 3, significant: true) assert_equal "9.99", number_helper.number_to_rounded(9.994, precision: 3, significant: true) assert_equal "11.0", number_helper.number_to_rounded(10.995, precision: 3, significant: true) - assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, precision: 20, significant: true ) - assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, precision: 20, significant: true ) - assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), precision: 20, significant: true ) - assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), precision: 20, significant: true ) - assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), precision: 20, significant: true ) - assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", precision: 20, significant: true ) - assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", precision: 100, significant: true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, precision: 20, significant: true) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, precision: 20, significant: true) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), precision: 20, significant: true) + assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), precision: 20, significant: true) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), precision: 20, significant: true) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", precision: 20, significant: true) + assert_equal "9775." + "0" * 96, number_helper.number_to_rounded("9775", precision: 100, significant: true) assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), precision: 3, significant: true)) end end def test_to_rounded_with_strip_insignificant_zeros [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| - assert_equal "9775.43", number_helper.number_to_rounded(9775.43, precision: 4, strip_insignificant_zeros: true ) - assert_equal "9775.2", number_helper.number_to_rounded(9775.2, precision: 6, significant: true, strip_insignificant_zeros: true ) - assert_equal "0", number_helper.number_to_rounded(0, precision: 6, significant: true, strip_insignificant_zeros: true ) + assert_equal "9775.43", number_helper.number_to_rounded(9775.43, precision: 4, strip_insignificant_zeros: true) + assert_equal "9775.2", number_helper.number_to_rounded(9775.2, precision: 6, significant: true, strip_insignificant_zeros: true) + assert_equal "0", number_helper.number_to_rounded(0, precision: 6, significant: true, strip_insignificant_zeros: true) end end @@ -210,8 +210,8 @@ module ActiveSupport # Zero precision with significant is a mistake (would always return zero), # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) assert_equal "124", number_helper.number_to_rounded(123.987, precision: 0, significant: true) - assert_equal "12", number_helper.number_to_rounded(12, precision: 0, significant: true ) - assert_equal "12", number_helper.number_to_rounded("12.3", precision: 0, significant: true ) + assert_equal "12", number_helper.number_to_rounded(12, precision: 0, significant: true) + assert_equal "12", number_helper.number_to_rounded("12.3", precision: 0, significant: true) end end @@ -244,23 +244,6 @@ module ActiveSupport end end - def test_number_to_human_size_with_si_prefix - assert_deprecated do - [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| - assert_equal "3 Bytes", number_helper.number_to_human_size(3.14159265, prefix: :si) - assert_equal "123 Bytes", number_helper.number_to_human_size(123.0, prefix: :si) - assert_equal "123 Bytes", number_helper.number_to_human_size(123, prefix: :si) - assert_equal "1.23 KB", number_helper.number_to_human_size(1234, prefix: :si) - assert_equal "12.3 KB", number_helper.number_to_human_size(12345, prefix: :si) - assert_equal "1.23 MB", number_helper.number_to_human_size(1234567, prefix: :si) - assert_equal "1.23 GB", number_helper.number_to_human_size(1234567890, prefix: :si) - assert_equal "1.23 TB", number_helper.number_to_human_size(1234567890123, prefix: :si) - assert_equal "1.23 PB", number_helper.number_to_human_size(1234567890123456, prefix: :si) - assert_equal "1.23 EB", number_helper.number_to_human_size(1234567890123456789, prefix: :si) - end - end - end - def test_number_to_human_size_with_options_hash [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| assert_equal "1.2 MB", number_helper.number_to_human_size(1234567, precision: 2) @@ -338,12 +321,18 @@ module ActiveSupport 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) + assert_equal "1000 hundred bucks", number_helper.number_to_human(100_000, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(999_999, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(1_000_000, units: gangster) 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 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 + 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_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 86da9f193a..2cefab3832 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -154,7 +154,7 @@ class OrderedHashTest < ActiveSupport::TestCase end def test_merge - other_hash = ActiveSupport::OrderedHash.new + other_hash = ActiveSupport::OrderedHash.new other_hash["purple"] = "800080" other_hash["violet"] = "ee82ee" merged = @ordered_hash.merge other_hash @@ -211,7 +211,7 @@ class OrderedHashTest < ActiveSupport::TestCase end def test_alternate_initialization_with_splat - alternate = ActiveSupport::OrderedHash[1,2,3,4] + alternate = ActiveSupport::OrderedHash[1, 2, 3, 4] assert_kind_of ActiveSupport::OrderedHash, alternate assert_equal [1, 3], alternate.keys end @@ -230,7 +230,7 @@ class OrderedHashTest < ActiveSupport::TestCase def test_alternate_initialization_raises_exception_on_odd_length_args assert_raises ArgumentError do - ActiveSupport::OrderedHash[1,2,3,4,5] + ActiveSupport::OrderedHash[1, 2, 3, 4, 5] end end diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb index 67d8c4b0e3..bdd80307c7 100644 --- a/activesupport/test/reloader_test.rb +++ b/activesupport/test/reloader_test.rb @@ -2,12 +2,15 @@ require "abstract_unit" class ReloaderTest < ActiveSupport::TestCase def test_prepare_callback - prepared = false + prepared = completed = false reloader.to_prepare { prepared = true } + reloader.to_complete { completed = true } assert !prepared + assert !completed reloader.prepare! assert prepared + assert !completed prepared = false reloader.wrap do @@ -17,6 +20,15 @@ class ReloaderTest < ActiveSupport::TestCase assert !prepared end + def test_prepend_prepare_callback + i = 10 + reloader.to_prepare { i += 1 } + reloader.to_prepare(prepend: true) { i = 0 } + + reloader.prepare! + assert_equal 1, i + end + def test_only_run_when_check_passes r = new_reloader { true } invoked = false diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index 7e5c3d1a8f..3bdd1651e7 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -43,7 +43,9 @@ class Stargate def dispatch(method) send(method) rescue Exception => e - rescue_with_handler(e) + unless rescue_with_handler(e) + @result = "unhandled" + end end def attack @@ -58,6 +60,26 @@ class Stargate raise MadRonon.new("dex") end + def crash + raise "unhandled RuntimeError" + end + + def looped_crash + ex1 = StandardError.new("error 1") + ex2 = StandardError.new("error 2") + begin + begin + raise ex1 + rescue + # sets the cause on ex2 to be ex1 + raise ex2 + end + rescue + # sets the cause on ex1 to be ex2 + raise ex1 + end + end + def fall_back_to_cause # This exception is the cause and has a handler. ronanize @@ -137,6 +159,16 @@ class RescuableTest < ActiveSupport::TestCase def test_rescue_falls_back_to_exception_cause @stargate.dispatch :fall_back_to_cause - assert_equal "unhandled RuntimeError with a handleable cause", @stargate.result + assert_equal "dex", @stargate.result + end + + def test_unhandled_exceptions + @stargate.dispatch(:crash) + assert_equal "unhandled", @stargate.result + end + + def test_rescue_handles_loops_in_exception_cause_chain + @stargate.dispatch :looped_crash + assert_equal "unhandled", @stargate.result end end diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 4d083ab773..36c068b91f 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -130,7 +130,7 @@ class SafeBufferTest < ActiveSupport::TestCase end test "Should be safe when sliced if original value was safe" do - new_buffer = @buffer[0,0] + new_buffer = @buffer[0, 0] assert_not_nil new_buffer assert new_buffer.html_safe?, "should be safe" end @@ -175,6 +175,6 @@ class SafeBufferTest < ActiveSupport::TestCase test "Should not affect frozen objects when accessing characters" do x = "Hello".html_safe - assert_equal x[/a/, 1], nil + assert_nil x[/a/, 1] end end diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb index 842bdd469d..e8f762da22 100644 --- a/activesupport/test/security_utils_test.rb +++ b/activesupport/test/security_utils_test.rb @@ -4,6 +4,11 @@ require "active_support/security_utils" class SecurityUtilsTest < ActiveSupport::TestCase def test_secure_compare_should_perform_string_comparison assert ActiveSupport::SecurityUtils.secure_compare("a", "a") - assert !ActiveSupport::SecurityUtils.secure_compare("a", "b") + 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") end end diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb index d41e4d6800..79a715349c 100644 --- a/activesupport/test/string_inquirer_test.rb +++ b/activesupport/test/string_inquirer_test.rb @@ -20,4 +20,25 @@ class StringInquirerTest < ActiveSupport::TestCase def test_respond_to assert_respond_to @string_inquirer, :development? end + + def test_respond_to_fallback_to_string_respond_to + String.class_eval do + def respond_to_missing?(name, include_private = false) + (name == :bar) || super + end + end + str = ActiveSupport::StringInquirer.new("hello") + + assert_respond_to str, :are_you_ready? + assert_respond_to str, :bar + assert_not_respond_to str, :nope + + ensure + String.class_eval do + undef_method :respond_to_missing? + def respond_to_missing?(name, include_private = false) + super + end + end + end end diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index d769a8c145..40dfbe2542 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -237,9 +237,6 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end -class AlsoDoingNothingTest < ActiveSupport::TestCase -end - # Setup and teardown callbacks. class SetupAndTeardownTest < ActiveSupport::TestCase setup :reset_callback_record, :foo @@ -257,7 +254,7 @@ class SetupAndTeardownTest < ActiveSupport::TestCase def teardown end - protected + private def reset_callback_record @called_back = [] @@ -282,7 +279,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter) end - protected + private def bar @called_back << :bar end diff --git a/activesupport/test/testing/file_fixtures_test.rb b/activesupport/test/testing/file_fixtures_test.rb index e44fe58ce9..9f28252c31 100644 --- a/activesupport/test/testing/file_fixtures_test.rb +++ b/activesupport/test/testing/file_fixtures_test.rb @@ -3,12 +3,12 @@ require "abstract_unit" require "pathname" class FileFixturesTest < ActiveSupport::TestCase - self.file_fixture_path = File.expand_path("../../file_fixtures", __FILE__) + self.file_fixture_path = File.expand_path("../file_fixtures", __dir__) test "#file_fixture returns Pathname to file fixture" do path = file_fixture("sample.txt") assert_kind_of Pathname, path - assert_match %r{.*/test/file_fixtures/sample.txt$}, path.to_s + assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s end test "raises an exception when the fixture file does not exist" do @@ -20,11 +20,11 @@ class FileFixturesTest < ActiveSupport::TestCase end class FileFixturesPathnameDirectoryTest < ActiveSupport::TestCase - self.file_fixture_path = Pathname.new(File.expand_path("../../file_fixtures", __FILE__)) + self.file_fixture_path = Pathname.new(File.expand_path("../file_fixtures", __dir__)) test "#file_fixture_path returns Pathname to file fixture" do path = file_fixture("sample.txt") assert_kind_of Pathname, path - assert_match %r{.*/test/file_fixtures/sample.txt$}, path.to_s + assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index c68be329bc..9d354f14f4 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -3,6 +3,10 @@ require "active_support/core_ext/date_time" require "active_support/core_ext/numeric/time" class TimeTravelTest < ActiveSupport::TestCase + class TimeSubclass < ::Time; end + class DateSubclass < ::Date; end + class DateTimeSubclass < ::DateTime; end + def test_time_helper_travel Time.stub(:now, Time.now) do begin @@ -90,11 +94,12 @@ class TimeTravelTest < ActiveSupport::TestCase outer_expected_time = Time.new(2004, 11, 24, 01, 04, 44) inner_expected_time = Time.new(2004, 10, 24, 01, 04, 44) travel_to outer_expected_time do - assert_raises(RuntimeError, /Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing./) do + e = assert_raises(RuntimeError) do travel_to(inner_expected_time) do #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) end end end @@ -142,4 +147,19 @@ class TimeTravelTest < ActiveSupport::TestCase end end end + + def test_time_helper_travel_with_time_subclass + assert_equal TimeSubclass, TimeSubclass.now.class + assert_equal DateSubclass, DateSubclass.today.class + assert_equal DateTimeSubclass, DateTimeSubclass.now.class + + travel 1.day do + assert_equal TimeSubclass, TimeSubclass.now.class + assert_equal DateSubclass, DateSubclass.today.class + assert_equal DateTimeSubclass, DateTimeSubclass.now.class + assert_equal Time.now.to_s, TimeSubclass.now.to_s + assert_equal Date.today.to_s, DateSubclass.today.to_s + assert_equal DateTime.now.to_s, DateTimeSubclass.now.to_s + end + end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 76fee1fdd4..de111cc40e 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -55,7 +55,7 @@ class TimeZoneTest < ActiveSupport::TestCase zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"].dup def zone.time_now; Time.local(2000); end assert_instance_of ActiveSupport::TimeWithZone, zone.now - assert_equal Time.utc(2000,1,1,5), zone.now.utc + assert_equal Time.utc(2000, 1, 1, 5), zone.now.utc assert_equal Time.utc(2000), zone.now.time assert_equal zone, zone.now.time_zone end @@ -65,10 +65,10 @@ class TimeZoneTest < ActiveSupport::TestCase with_env_tz "US/Eastern" do zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"].dup def zone.time_now - Time.local(2006,4,2,2) # 2AM springs forward to 3AM + Time.local(2006, 4, 2, 2) # 2AM springs forward to 3AM end - assert_equal Time.utc(2006,4,2,3), zone.now.time + assert_equal Time.utc(2006, 4, 2, 3), zone.now.time assert_equal true, zone.now.dst? end end @@ -79,7 +79,7 @@ class TimeZoneTest < ActiveSupport::TestCase def zone.time_now Time.at(1162098000) # equivalent to 1AM DST end - assert_equal Time.utc(2006,10,29,1), zone.now.time + assert_equal Time.utc(2006, 10, 29, 1), zone.now.time assert_equal true, zone.now.dst? end end @@ -162,25 +162,25 @@ class TimeZoneTest < ActiveSupport::TestCase def test_local_with_old_date time = ActiveSupport::TimeZone["Hawaii"].local(1850, 2, 5, 15, 30, 45) - assert_equal [45,30,15,5,2,1850], time.to_a[0,6] + assert_equal [45, 30, 15, 5, 2, 1850], time.to_a[0, 6] assert_equal ActiveSupport::TimeZone["Hawaii"], time.time_zone end def test_local_enforces_spring_dst_rules zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] - twz = zone.local(2006,4,2,1,59,59) # 1 second before DST start - assert_equal Time.utc(2006,4,2,1,59,59), twz.time - assert_equal Time.utc(2006,4,2,6,59,59), twz.utc + twz = zone.local(2006, 4, 2, 1, 59, 59) # 1 second before DST start + assert_equal Time.utc(2006, 4, 2, 1, 59, 59), twz.time + assert_equal Time.utc(2006, 4, 2, 6, 59, 59), twz.utc assert_equal false, twz.dst? assert_equal "EST", twz.zone - twz2 = zone.local(2006,4,2,2) # 2AM does not exist because at 2AM, time springs forward to 3AM - assert_equal Time.utc(2006,4,2,3), twz2.time # twz is created for 3AM - assert_equal Time.utc(2006,4,2,7), twz2.utc + twz2 = zone.local(2006, 4, 2, 2) # 2AM does not exist because at 2AM, time springs forward to 3AM + assert_equal Time.utc(2006, 4, 2, 3), twz2.time # twz is created for 3AM + assert_equal Time.utc(2006, 4, 2, 7), twz2.utc assert_equal true, twz2.dst? assert_equal "EDT", twz2.zone - twz3 = zone.local(2006,4,2,2,30) # 2:30AM does not exist because at 2AM, time springs forward to 3AM - assert_equal Time.utc(2006,4,2,3,30), twz3.time # twz is created for 3:30AM - assert_equal Time.utc(2006,4,2,7,30), twz3.utc + twz3 = zone.local(2006, 4, 2, 2, 30) # 2:30AM does not exist because at 2AM, time springs forward to 3AM + assert_equal Time.utc(2006, 4, 2, 3, 30), twz3.time # twz is created for 3:30AM + assert_equal Time.utc(2006, 4, 2, 7, 30), twz3.utc assert_equal true, twz3.dst? assert_equal "EDT", twz3.zone end @@ -189,9 +189,9 @@ class TimeZoneTest < ActiveSupport::TestCase # 1AM during fall DST transition is ambiguous, it could be either DST or non-DST 1AM # Mirroring Time.local behavior, this method selects the DST time zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] - twz = zone.local(2006,10,29,1) - assert_equal Time.utc(2006,10,29,1), twz.time - assert_equal Time.utc(2006,10,29,5), twz.utc + twz = zone.local(2006, 10, 29, 1) + assert_equal Time.utc(2006, 10, 29, 1), twz.time + assert_equal Time.utc(2006, 10, 29, 5), twz.utc assert_equal true, twz.dst? assert_equal "EDT", twz.zone end @@ -200,7 +200,7 @@ class TimeZoneTest < ActiveSupport::TestCase zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] secs = 946684800.0 twz = zone.at(secs) - assert_equal Time.utc(1999,12,31,19), twz.time + assert_equal Time.utc(1999, 12, 31, 19), twz.time assert_equal Time.utc(2000), twz.utc assert_equal zone, twz.time_zone assert_equal secs, twz.to_f @@ -215,10 +215,99 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal secs, twz.to_f end + def test_iso8601 + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("1999-12-31T19:00:00") + assert_equal Time.utc(1999, 12, 31, 19), twz.time + assert_equal Time.utc(2000), twz.utc + assert_equal zone, twz.time_zone + end + + def test_iso8601_with_fractional_seconds + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("1999-12-31T19:00:00.750") + assert_equal 750000, twz.time.usec + assert_equal Time.utc(1999, 12, 31, 19, 0, 0 + Rational(3, 4)), twz.time + assert_equal Time.utc(2000, 1, 1, 0, 0, 0 + Rational(3, 4)), twz.utc + assert_equal zone, twz.time_zone + end + + def test_iso8601_with_zone + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("1999-12-31T14:00:00-10:00") + assert_equal Time.utc(1999, 12, 31, 19), twz.time + assert_equal Time.utc(2000), twz.utc + assert_equal zone, twz.time_zone + end + + def test_iso8601_with_invalid_string + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + + exception = assert_raises(ArgumentError) do + zone.iso8601("foobar") + end + + assert_equal "invalid date", exception.message + end + + def test_iso8601_with_missing_time_components + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("1999-12-31") + assert_equal Time.utc(1999, 12, 31, 0, 0, 0), twz.time + assert_equal Time.utc(1999, 12, 31, 5, 0, 0), twz.utc + assert_equal zone, twz.time_zone + end + + def test_iso8601_with_old_date + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("1883-12-31T19:00:00") + assert_equal [0, 0, 19, 31, 12, 1883], twz.to_a[0, 6] + assert_equal zone, twz.time_zone + end + + def test_iso8601_far_future_date_with_time_zone_offset_in_string + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("2050-12-31T19:00:00-10:00") # i.e., 2050-01-01 05:00:00 UTC + assert_equal [0, 0, 0, 1, 1, 2051], twz.to_a[0, 6] + assert_equal zone, twz.time_zone + end + + def test_iso8601_should_not_black_out_system_timezone_dst_jump + with_env_tz("EET") do + zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] + twz = zone.iso8601("2012-03-25T03:29:00") + assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0, 6] + end + end + + def test_iso8601_should_black_out_app_timezone_dst_jump + with_env_tz("EET") do + zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] + twz = zone.iso8601("2012-03-11T02:29:00") + assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0, 6] + end + end + + def test_iso8601_doesnt_use_local_dst + with_env_tz "US/Eastern" do + zone = ActiveSupport::TimeZone["UTC"] + twz = zone.iso8601("2013-03-10T02:00:00") + assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time + end + end + + def test_iso8601_handles_dst_jump + with_env_tz "US/Eastern" do + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("2013-03-10T02:00:00") + assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time + end + end + def test_parse zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.parse("1999-12-31 19:00:00") - assert_equal Time.utc(1999,12,31,19), twz.time + assert_equal Time.utc(1999, 12, 31, 19), twz.time assert_equal Time.utc(2000), twz.utc assert_equal zone, twz.time_zone end @@ -234,14 +323,14 @@ class TimeZoneTest < ActiveSupport::TestCase def test_parse_with_old_date zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.parse("1883-12-31 19:00:00") - assert_equal [0,0,19,31,12,1883], twz.to_a[0,6] + assert_equal [0, 0, 19, 31, 12, 1883], twz.to_a[0, 6] assert_equal zone, twz.time_zone end def test_parse_far_future_date_with_time_zone_offset_in_string zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.parse("2050-12-31 19:00:00 -10:00") # i.e., 2050-01-01 05:00:00 UTC - assert_equal [0,0,0,1,1,2051], twz.to_a[0,6] + assert_equal [0, 0, 0, 1, 1, 2051], twz.to_a[0, 6] assert_equal zone, twz.time_zone end @@ -253,9 +342,9 @@ class TimeZoneTest < ActiveSupport::TestCase def test_parse_with_incomplete_date zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] - zone.stub(:now, zone.local(1999,12,31)) do + zone.stub(:now, zone.local(1999, 12, 31)) do twz = zone.parse("19:00:00") - assert_equal Time.utc(1999,12,31,19), twz.time + assert_equal Time.utc(1999, 12, 31, 19), twz.time end end @@ -272,7 +361,7 @@ class TimeZoneTest < ActiveSupport::TestCase with_env_tz("EET") do zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] twz = zone.parse("2012-03-25 03:29:00") - assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0,6] + assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0, 6] end end @@ -280,7 +369,7 @@ class TimeZoneTest < ActiveSupport::TestCase with_env_tz("EET") do zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] twz = zone.parse("2012-03-11 02:29:00") - assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0,6] + assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0, 6] end end @@ -314,12 +403,105 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_rfc3339 + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.rfc3339("1999-12-31T14:00:00-10:00") + assert_equal Time.utc(1999, 12, 31, 19), twz.time + assert_equal Time.utc(2000), twz.utc + assert_equal zone, twz.time_zone + end + + def test_rfc3339_with_fractional_seconds + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("1999-12-31T14:00:00.750-10:00") + assert_equal 750000, twz.time.usec + assert_equal Time.utc(1999, 12, 31, 19, 0, 0 + Rational(3, 4)), twz.time + assert_equal Time.utc(2000, 1, 1, 0, 0, 0 + Rational(3, 4)), twz.utc + assert_equal zone, twz.time_zone + end + + def test_rfc3339_with_missing_time + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + + exception = assert_raises(ArgumentError) do + zone.rfc3339("1999-12-31") + end + + assert_equal "invalid date", exception.message + end + + def test_rfc3339_with_missing_offset + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + + exception = assert_raises(ArgumentError) do + zone.rfc3339("1999-12-31T19:00:00") + end + + assert_equal "invalid date", exception.message + end + + def test_rfc3339_with_invalid_string + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + + exception = assert_raises(ArgumentError) do + zone.rfc3339("foobar") + end + + assert_equal "invalid date", exception.message + end + + def test_rfc3339_with_old_date + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.rfc3339("1883-12-31T19:00:00-05:00") + assert_equal [0, 0, 19, 31, 12, 1883], twz.to_a[0, 6] + assert_equal zone, twz.time_zone + end + + def test_rfc3339_far_future_date_with_time_zone_offset_in_string + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.rfc3339("2050-12-31T19:00:00-10:00") # i.e., 2050-01-01 05:00:00 UTC + assert_equal [0, 0, 0, 1, 1, 2051], twz.to_a[0, 6] + assert_equal zone, twz.time_zone + end + + def test_rfc3339_should_not_black_out_system_timezone_dst_jump + with_env_tz("EET") do + zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] + twz = zone.rfc3339("2012-03-25T03:29:00-07:00") + assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0, 6] + end + end + + def test_rfc3339_should_black_out_app_timezone_dst_jump + with_env_tz("EET") do + zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] + twz = zone.rfc3339("2012-03-11T02:29:00-08:00") + assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0, 6] + end + end + + def test_rfc3339_doesnt_use_local_dst + with_env_tz "US/Eastern" do + zone = ActiveSupport::TimeZone["UTC"] + twz = zone.rfc3339("2013-03-10T02:00:00Z") + assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time + end + end + + def test_rfc3339_handles_dst_jump + with_env_tz "US/Eastern" do + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + twz = zone.iso8601("2013-03-10T02:00:00-05:00") + assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time + end + end + def test_strptime zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00", "%Y-%m-%d %H:%M:%S") - assert_equal Time.utc(1999,12,31,17), twz - assert_equal Time.utc(1999,12,31,12), twz.time - assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal Time.utc(1999, 12, 31, 17), twz + assert_equal Time.utc(1999, 12, 31, 12), twz.time + assert_equal Time.utc(1999, 12, 31, 17), twz.utc assert_equal zone, twz.time_zone end @@ -327,9 +509,9 @@ class TimeZoneTest < ActiveSupport::TestCase with_tz_default ActiveSupport::TimeZone["Pacific Time (US & Canada)"] do zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00", "%Y-%m-%d %H:%M:%S") - assert_equal Time.utc(1999,12,31,17), twz - assert_equal Time.utc(1999,12,31,12), twz.time - assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal Time.utc(1999, 12, 31, 17), twz + assert_equal Time.utc(1999, 12, 31, 12), twz.time + assert_equal Time.utc(1999, 12, 31, 17), twz.utc assert_equal zone, twz.time_zone end end @@ -337,45 +519,45 @@ class TimeZoneTest < ActiveSupport::TestCase def test_strptime_with_explicit_time_zone_as_abbrev zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00 PST", "%Y-%m-%d %H:%M:%S %Z") - assert_equal Time.utc(1999,12,31,20), twz - assert_equal Time.utc(1999,12,31,15), twz.time - assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal Time.utc(1999, 12, 31, 20), twz + assert_equal Time.utc(1999, 12, 31, 15), twz.time + assert_equal Time.utc(1999, 12, 31, 20), twz.utc assert_equal zone, twz.time_zone end def test_strptime_with_explicit_time_zone_as_h_offset zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00 -08", "%Y-%m-%d %H:%M:%S %:::z") - assert_equal Time.utc(1999,12,31,20), twz - assert_equal Time.utc(1999,12,31,15), twz.time - assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal Time.utc(1999, 12, 31, 20), twz + assert_equal Time.utc(1999, 12, 31, 15), twz.time + assert_equal Time.utc(1999, 12, 31, 20), twz.utc assert_equal zone, twz.time_zone end def test_strptime_with_explicit_time_zone_as_hm_offset zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00 -08:00", "%Y-%m-%d %H:%M:%S %:z") - assert_equal Time.utc(1999,12,31,20), twz - assert_equal Time.utc(1999,12,31,15), twz.time - assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal Time.utc(1999, 12, 31, 20), twz + assert_equal Time.utc(1999, 12, 31, 15), twz.time + assert_equal Time.utc(1999, 12, 31, 20), twz.utc assert_equal zone, twz.time_zone end def test_strptime_with_explicit_time_zone_as_hms_offset zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00 -08:00:00", "%Y-%m-%d %H:%M:%S %::z") - assert_equal Time.utc(1999,12,31,20), twz - assert_equal Time.utc(1999,12,31,15), twz.time - assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal Time.utc(1999, 12, 31, 20), twz + assert_equal Time.utc(1999, 12, 31, 15), twz.time + assert_equal Time.utc(1999, 12, 31, 20), twz.utc assert_equal zone, twz.time_zone end def test_strptime_with_almost_explicit_time_zone zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.strptime("1999-12-31 12:00:00 %Z", "%Y-%m-%d %H:%M:%S %%Z") - assert_equal Time.utc(1999,12,31,17), twz - assert_equal Time.utc(1999,12,31,12), twz.time - assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal Time.utc(1999, 12, 31, 17), twz + assert_equal Time.utc(1999, 12, 31, 12), twz.time + assert_equal Time.utc(1999, 12, 31, 17), twz.utc assert_equal zone, twz.time_zone end @@ -395,10 +577,28 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_strptime_with_timestamp_seconds + with_env_tz "US/Eastern" do + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + time_str = "1470272280" + time = zone.strptime(time_str, "%s") + assert_equal Time.at(1470272280), time + end + end + + def test_strptime_with_timestamp_milliseconds + with_env_tz "US/Eastern" do + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + time_str = "1470272280000" + time = zone.strptime(time_str, "%Q") + assert_equal Time.at(1470272280), time + end + 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) - assert_equal nil, zone.instance_variable_get("@utc_offset") + assert_nil zone.instance_variable_get("@utc_offset") assert_equal(-18_000, zone.utc_offset) end @@ -478,8 +678,8 @@ class TimeZoneTest < ActiveSupport::TestCase def test_all_sorted all = ActiveSupport::TimeZone.all - 1.upto( all.length-1 ) do |i| - assert all[i-1] < all[i] + 1.upto(all.length - 1) do |i| + assert all[i - 1] < all[i] end end @@ -514,6 +714,10 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"] end + def test_country_zones_without_mappings + assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"] + end + def test_to_yaml assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n", ActiveSupport::TimeZone["Hawaii"].to_yaml) assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Europe/London\n", ActiveSupport::TimeZone["Europe/London"].to_yaml) diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index 040ddd25fc..466b69bcef 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -31,4 +31,22 @@ class TransliterateTest < ActiveSupport::TestCase def test_transliterate_should_allow_a_custom_replacement_char assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*") end + + def test_transliterate_handles_empty_string + assert_equal "", ActiveSupport::Inflector.transliterate("") + end + + def test_transliterate_handles_nil + exception = assert_raises ArgumentError do + ActiveSupport::Inflector.transliterate(nil) + end + assert_equal "Can only transliterate strings. Received NilClass", exception.message + end + + def test_transliterate_handles_unknown_object + exception = assert_raises ArgumentError do + ActiveSupport::Inflector.transliterate(Object.new) + end + assert_equal "Can only transliterate strings. Received Object", exception.message + end end diff --git a/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb index 816d57972c..fc35ac113b 100644 --- a/activesupport/test/xml_mini/jdom_engine_test.rb +++ b/activesupport/test/xml_mini/jdom_engine_test.rb @@ -1,36 +1,8 @@ -if RUBY_PLATFORM.include?("java") - require "abstract_unit" - require "active_support/xml_mini" - require "active_support/core_ext/hash/conversions" +require_relative "xml_mini_engine_test" - class JDOMEngineTest < ActiveSupport::TestCase - include ActiveSupport - - FILES_DIR = File.dirname(__FILE__) + "/../fixtures/xml" - - def setup - @default_backend = XmlMini.backend - XmlMini.backend = "JDOM" - end - - def teardown - XmlMini.backend = @default_backend - end - - def test_file_from_xml - hash = Hash.from_xml(<<-eoxml) - <blog> - <logo type="file" name="logo.png" content_type="image/png"> - </logo> - </blog> - eoxml - assert hash.has_key?("blog") - assert hash["blog"].has_key?("logo") - - file = hash["blog"]["logo"] - assert_equal "logo.png", file.original_filename - assert_equal "image/png", file.content_type - end +XMLMiniEngineTest.run_with_platform("java") do + class JDOMEngineTest < XMLMiniEngineTest + FILES_DIR = File.expand_path("../fixtures/xml", __dir__) def test_not_allowed_to_expand_entities_to_files attack_xml = <<-EOT @@ -63,121 +35,17 @@ if RUBY_PLATFORM.include?("java") assert_equal "x", Hash.from_xml(attack_xml)["member"] end - def test_exception_thrown_on_expansion_attack - assert_raise Java::OrgXmlSax::SAXParseException do - attack_xml = <<-EOT - <!DOCTYPE member [ - <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> - <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> - <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> - <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> - <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> - <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> - <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> - ]> - <member> - &a; - </member> - EOT - Hash.from_xml(attack_xml) + private + def engine + "JDOM" end - end - - def test_setting_JDOM_as_backend - XmlMini.backend = "JDOM" - assert_equal XmlMini_JDOM, XmlMini.backend - end - - def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse("")) - end - - def test_array_type_makes_an_array - assert_equal_rexml(<<-eoxml) - <blog> - <posts type="array"> - <post>a post</post> - <post>another post</post> - </posts> - </blog> - eoxml - end - - def test_one_node_document_as_hash - assert_equal_rexml(<<-eoxml) - <products/> - eoxml - end - - def test_one_node_with_attributes_document_as_hash - assert_equal_rexml(<<-eoxml) - <products foo="bar"/> - eoxml - end - - def test_products_node_with_book_node_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - </products> - eoxml - end - - def test_products_node_with_two_book_nodes_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - <book name="america" id="67890" /> - </products> - eoxml - end - def test_single_node_with_content_as_hash - assert_equal_rexml(<<-eoxml) - <products> - hello world - </products> - eoxml - end - - def test_children_with_children - assert_equal_rexml(<<-eoxml) - <root> - <products> - <book name="america" id="67890" /> - </products> - </root> - eoxml - end - - def test_children_with_text - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello everyone - </products> - </root> - eoxml - end - - def test_children_with_non_adjacent_text - assert_equal_rexml(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - end + def expansion_attack_error + Java::OrgXmlSax::SAXParseException + end - private - def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) - hash = XmlMini.with_backend("REXML") { XmlMini.parse(xml) } - assert_equal(hash, parsed_xml) + def extended_engine? + false end end end diff --git a/activesupport/test/xml_mini/libxml_engine_test.rb b/activesupport/test/xml_mini/libxml_engine_test.rb index 81b0d3c407..f3394ad7f2 100644 --- a/activesupport/test/xml_mini/libxml_engine_test.rb +++ b/activesupport/test/xml_mini/libxml_engine_test.rb @@ -1,203 +1,19 @@ -begin - require "libxml" -rescue LoadError - # Skip libxml tests -else - require "abstract_unit" - require "active_support/xml_mini" - require "active_support/core_ext/hash/conversions" - - class LibxmlEngineTest < ActiveSupport::TestCase - include ActiveSupport +require_relative "xml_mini_engine_test" +XMLMiniEngineTest.run_with_gem("libxml") do + class LibxmlEngineTest < XMLMiniEngineTest def setup - @default_backend = XmlMini.backend - XmlMini.backend = "LibXML" - + super LibXML::XML::Error.set_handler(&lambda { |error| }) #silence libxml, exceptions will do end - def teardown - XmlMini.backend = @default_backend - end - - def test_exception_thrown_on_expansion_attack - assert_raise LibXML::XML::Error do - attack_xml = %{<?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE member [ - <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> - <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> - <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> - <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> - <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> - <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> - <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> - ]> - <member> - &a; - </member> - } - Hash.from_xml(attack_xml) + private + def engine + "LibXML" end - end - - def test_setting_libxml_as_backend - XmlMini.backend = "LibXML" - assert_equal XmlMini_LibXML, XmlMini.backend - end - - def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse("")) - end - - def test_array_type_makes_an_array - assert_equal_rexml(<<-eoxml) - <blog> - <posts type="array"> - <post>a post</post> - <post>another post</post> - </posts> - </blog> - eoxml - end - - def test_one_node_document_as_hash - assert_equal_rexml(<<-eoxml) - <products/> - eoxml - end - - def test_one_node_with_attributes_document_as_hash - assert_equal_rexml(<<-eoxml) - <products foo="bar"/> - eoxml - end - - def test_products_node_with_book_node_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - </products> - eoxml - end - - def test_products_node_with_two_book_nodes_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - <book name="america" id="67890" /> - </products> - eoxml - end - - def test_single_node_with_content_as_hash - assert_equal_rexml(<<-eoxml) - <products> - hello world - </products> - eoxml - end - - def test_children_with_children - assert_equal_rexml(<<-eoxml) - <root> - <products> - <book name="america" id="67890" /> - </products> - </root> - eoxml - end - - def test_children_with_text - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello everyone - </products> - </root> - eoxml - end - - def test_children_with_non_adjacent_text - assert_equal_rexml(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - end - - def test_parse_from_io - io = StringIO.new(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - assert_equal_rexml(io) - end - - def test_children_with_simple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock]]> - </products> - </root> - eoxml - end - def test_children_with_multiple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]> - </products> - </root> - eoxml - end - - def test_children_with_text_and_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello <![CDATA[cdatablock]]> - morning - </products> - </root> - eoxml - end - - def test_children_with_blank_text - assert_equal_rexml(<<-eoxml) - <root> - <products> </products> - </root> - eoxml - end - - def test_children_with_blank_text_and_attribute - assert_equal_rexml(<<-eoxml) - <root> - <products type="file"> </products> - </root> - eoxml - end - - private - def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) - xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend("REXML") { XmlMini.parse(xml) } - assert_equal(hash, parsed_xml) + def expansion_attack_error + LibXML::XML::Error end end - end diff --git a/activesupport/test/xml_mini/libxmlsax_engine_test.rb b/activesupport/test/xml_mini/libxmlsax_engine_test.rb index e25fa2813c..f457e160d6 100644 --- a/activesupport/test/xml_mini/libxmlsax_engine_test.rb +++ b/activesupport/test/xml_mini/libxmlsax_engine_test.rb @@ -1,195 +1,14 @@ -begin - require "libxml" -rescue LoadError - # Skip libxml tests -else - require "abstract_unit" - require "active_support/xml_mini" - require "active_support/core_ext/hash/conversions" +require_relative "xml_mini_engine_test" - class LibXMLSAXEngineTest < ActiveSupport::TestCase - include ActiveSupport - - def setup - @default_backend = XmlMini.backend - XmlMini.backend = "LibXMLSAX" - end - - def teardown - XmlMini.backend = @default_backend - end - - def test_exception_thrown_on_expansion_attack - assert_raise LibXML::XML::Error do - attack_xml = <<-EOT - <?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE member [ - <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> - <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> - <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> - <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> - <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> - <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> - <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> - ]> - <member> - &a; - </member> - EOT - - Hash.from_xml(attack_xml) +XMLMiniEngineTest.run_with_gem("libxml") do + class LibXMLSAXEngineTest < XMLMiniEngineTest + private + def engine + "LibXMLSAX" end - end - - def test_setting_libxml_as_backend - XmlMini.backend = "LibXMLSAX" - assert_equal XmlMini_LibXMLSAX, XmlMini.backend - end - - def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse("")) - end - - def test_array_type_makes_an_array - assert_equal_rexml(<<-eoxml) - <blog> - <posts type="array"> - <post>a post</post> - <post>another post</post> - </posts> - </blog> - eoxml - end - - def test_one_node_document_as_hash - assert_equal_rexml(<<-eoxml) - <products/> - eoxml - end - - def test_one_node_with_attributes_document_as_hash - assert_equal_rexml(<<-eoxml) - <products foo="bar"/> - eoxml - end - - def test_products_node_with_book_node_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - </products> - eoxml - end - - def test_products_node_with_two_book_nodes_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - <book name="america" id="67890" /> - </products> - eoxml - end - def test_single_node_with_content_as_hash - assert_equal_rexml(<<-eoxml) - <products> - hello world - </products> - eoxml - end - - def test_children_with_children - assert_equal_rexml(<<-eoxml) - <root> - <products> - <book name="america" id="67890" /> - </products> - </root> - eoxml - end - - def test_children_with_text - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello everyone - </products> - </root> - eoxml - end - - def test_children_with_non_adjacent_text - assert_equal_rexml(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - end - - def test_parse_from_io - io = StringIO.new(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - assert_equal_rexml(io) - end - - def test_children_with_simple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock]]> - </products> - </root> - eoxml - end - - def test_children_with_multiple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]> - </products> - </root> - eoxml - end - - def test_children_with_text_and_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello <![CDATA[cdatablock]]> - morning - </products> - </root> - eoxml - end - - def test_children_with_blank_text - assert_equal_rexml(<<-eoxml) - <root> - <products> </products> - </root> - eoxml - end - - private - def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) - xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend("REXML") { XmlMini.parse(xml) } - assert_equal(hash, parsed_xml) + def expansion_attack_error + LibXML::XML::Error end end - end diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb index 44b82da4e4..3151e75fc0 100644 --- a/activesupport/test/xml_mini/nokogiri_engine_test.rb +++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb @@ -1,215 +1,14 @@ -begin - require "nokogiri" -rescue LoadError - # Skip nokogiri tests -else - require "abstract_unit" - require "active_support/xml_mini" - require "active_support/core_ext/hash/conversions" +require_relative "xml_mini_engine_test" - class NokogiriEngineTest < ActiveSupport::TestCase - def setup - @default_backend = ActiveSupport::XmlMini.backend - ActiveSupport::XmlMini.backend = "Nokogiri" - end - - def teardown - ActiveSupport::XmlMini.backend = @default_backend - end - - def test_file_from_xml - hash = Hash.from_xml(<<-eoxml) - <blog> - <logo type="file" name="logo.png" content_type="image/png"> - </logo> - </blog> - eoxml - assert hash.has_key?("blog") - assert hash["blog"].has_key?("logo") - - file = hash["blog"]["logo"] - assert_equal "logo.png", file.original_filename - assert_equal "image/png", file.content_type - end - - def test_exception_thrown_on_expansion_attack - assert_raise Nokogiri::XML::SyntaxError do - attack_xml = <<-EOT - <?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE member [ - <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> - <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> - <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> - <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> - <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> - <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> - <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> - ]> - <member> - &a; - </member> - EOT - Hash.from_xml(attack_xml) +XMLMiniEngineTest.run_with_gem("nokogiri") do + class NokogiriEngineTest < XMLMiniEngineTest + private + def engine + "Nokogiri" end - end - - def test_setting_nokogiri_as_backend - ActiveSupport::XmlMini.backend = "Nokogiri" - assert_equal ActiveSupport::XmlMini_Nokogiri, ActiveSupport::XmlMini.backend - end - - def test_blank_returns_empty_hash - assert_equal({}, ActiveSupport::XmlMini.parse(nil)) - assert_equal({}, ActiveSupport::XmlMini.parse("")) - end - - def test_array_type_makes_an_array - assert_equal_rexml(<<-eoxml) - <blog> - <posts type="array"> - <post>a post</post> - <post>another post</post> - </posts> - </blog> - eoxml - end - - def test_one_node_document_as_hash - assert_equal_rexml(<<-eoxml) - <products/> - eoxml - end - - def test_one_node_with_attributes_document_as_hash - assert_equal_rexml(<<-eoxml) - <products foo="bar"/> - eoxml - end - - def test_products_node_with_book_node_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - </products> - eoxml - end - - def test_products_node_with_two_book_nodes_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - <book name="america" id="67890" /> - </products> - eoxml - end - def test_single_node_with_content_as_hash - assert_equal_rexml(<<-eoxml) - <products> - hello world - </products> - eoxml - end - - def test_children_with_children - assert_equal_rexml(<<-eoxml) - <root> - <products> - <book name="america" id="67890" /> - </products> - </root> - eoxml - end - - def test_children_with_text - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello everyone - </products> - </root> - eoxml - end - - def test_children_with_non_adjacent_text - assert_equal_rexml(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - end - - def test_parse_from_io - io = StringIO.new(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - assert_equal_rexml(io) - end - - def test_children_with_simple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock]]> - </products> - </root> - eoxml - end - - def test_children_with_multiple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]> - </products> - </root> - eoxml - end - - def test_children_with_text_and_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello <![CDATA[cdatablock]]> - morning - </products> - </root> - eoxml - end - - def test_children_with_blank_text - assert_equal_rexml(<<-eoxml) - <root> - <products> </products> - </root> - eoxml - end - - def test_children_with_blank_text_and_attribute - assert_equal_rexml(<<-eoxml) - <root> - <products type="file"> </products> - </root> - eoxml - end - - private - def assert_equal_rexml(xml) - parsed_xml = ActiveSupport::XmlMini.parse(xml) - xml.rewind if xml.respond_to?(:rewind) - hash = ActiveSupport::XmlMini.with_backend("REXML") { ActiveSupport::XmlMini.parse(xml) } - assert_equal(hash, parsed_xml) + def expansion_attack_error + Nokogiri::XML::SyntaxError end end - end diff --git a/activesupport/test/xml_mini/nokogirisax_engine_test.rb b/activesupport/test/xml_mini/nokogirisax_engine_test.rb index 24b35cadf6..7dafbdaf48 100644 --- a/activesupport/test/xml_mini/nokogirisax_engine_test.rb +++ b/activesupport/test/xml_mini/nokogirisax_engine_test.rb @@ -1,216 +1,14 @@ -begin - require "nokogiri" -rescue LoadError - # Skip nokogiri tests -else - require "abstract_unit" - require "active_support/xml_mini" - require "active_support/core_ext/hash/conversions" +require_relative "xml_mini_engine_test" - class NokogiriSAXEngineTest < ActiveSupport::TestCase - def setup - @default_backend = ActiveSupport::XmlMini.backend - ActiveSupport::XmlMini.backend = "NokogiriSAX" - end - - def teardown - ActiveSupport::XmlMini.backend = @default_backend - end - - def test_file_from_xml - hash = Hash.from_xml(<<-eoxml) - <blog> - <logo type="file" name="logo.png" content_type="image/png"> - </logo> - </blog> - eoxml - assert hash.has_key?("blog") - assert hash["blog"].has_key?("logo") - - file = hash["blog"]["logo"] - assert_equal "logo.png", file.original_filename - assert_equal "image/png", file.content_type - end - - def test_exception_thrown_on_expansion_attack - assert_raise RuntimeError do - attack_xml = <<-EOT - <?xml version="1.0" encoding="UTF-8"?> - <!DOCTYPE member [ - <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> - <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> - <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> - <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> - <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> - <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> - <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> - ]> - <member> - &a; - </member> - EOT - - Hash.from_xml(attack_xml) +XMLMiniEngineTest.run_with_gem("nokogiri") do + class NokogiriSAXEngineTest < XMLMiniEngineTest + private + def engine + "NokogiriSAX" end - end - - def test_setting_nokogirisax_as_backend - ActiveSupport::XmlMini.backend = "NokogiriSAX" - assert_equal ActiveSupport::XmlMini_NokogiriSAX, ActiveSupport::XmlMini.backend - end - - def test_blank_returns_empty_hash - assert_equal({}, ActiveSupport::XmlMini.parse(nil)) - assert_equal({}, ActiveSupport::XmlMini.parse("")) - end - - def test_array_type_makes_an_array - assert_equal_rexml(<<-eoxml) - <blog> - <posts type="array"> - <post>a post</post> - <post>another post</post> - </posts> - </blog> - eoxml - end - - def test_one_node_document_as_hash - assert_equal_rexml(<<-eoxml) - <products/> - eoxml - end - - def test_one_node_with_attributes_document_as_hash - assert_equal_rexml(<<-eoxml) - <products foo="bar"/> - eoxml - end - - def test_products_node_with_book_node_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - </products> - eoxml - end - - def test_products_node_with_two_book_nodes_as_hash - assert_equal_rexml(<<-eoxml) - <products> - <book name="awesome" id="12345" /> - <book name="america" id="67890" /> - </products> - eoxml - end - def test_single_node_with_content_as_hash - assert_equal_rexml(<<-eoxml) - <products> - hello world - </products> - eoxml - end - - def test_children_with_children - assert_equal_rexml(<<-eoxml) - <root> - <products> - <book name="america" id="67890" /> - </products> - </root> - eoxml - end - - def test_children_with_text - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello everyone - </products> - </root> - eoxml - end - - def test_children_with_non_adjacent_text - assert_equal_rexml(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - end - - def test_parse_from_io - io = StringIO.new(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - assert_equal_rexml(io) - end - - def test_children_with_simple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock]]> - </products> - </root> - eoxml - end - - def test_children_with_multiple_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]> - </products> - </root> - eoxml - end - - def test_children_with_text_and_cdata - assert_equal_rexml(<<-eoxml) - <root> - <products> - hello <![CDATA[cdatablock]]> - morning - </products> - </root> - eoxml - end - - def test_children_with_blank_text - assert_equal_rexml(<<-eoxml) - <root> - <products> </products> - </root> - eoxml - end - - def test_children_with_blank_text_and_attribute - assert_equal_rexml(<<-eoxml) - <root> - <products type="file"> </products> - </root> - eoxml - end - - private - def assert_equal_rexml(xml) - parsed_xml = ActiveSupport::XmlMini.parse(xml) - xml.rewind if xml.respond_to?(:rewind) - hash = ActiveSupport::XmlMini.with_backend("REXML") { ActiveSupport::XmlMini.parse(xml) } - assert_equal(hash, parsed_xml) + def expansion_attack_error + RuntimeError end end - end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index dc62f3f671..c51f0d3c20 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -1,44 +1,25 @@ -require "abstract_unit" -require "active_support/xml_mini" +require_relative "xml_mini_engine_test" -class REXMLEngineTest < ActiveSupport::TestCase +class REXMLEngineTest < XMLMiniEngineTest def test_default_is_rexml assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend end - def test_set_rexml_as_backend - ActiveSupport::XmlMini.backend = "REXML" - assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend - end - - def test_parse_from_io - ActiveSupport::XmlMini.backend = "REXML" - io = StringIO.new(<<-eoxml) - <root> - good - <products> - hello everyone - </products> - morning - </root> - eoxml - hash = ActiveSupport::XmlMini.parse(io) - assert hash.has_key?("root") - assert hash["root"].has_key?("products") - assert_match "good", hash["root"]["__content__"] - products = hash["root"]["products"] - assert products.has_key?("__content__") - assert_match "hello everyone", products["__content__"] - end - def test_parse_from_empty_string - ActiveSupport::XmlMini.backend = "REXML" assert_equal({}, ActiveSupport::XmlMini.parse("")) end def test_parse_from_frozen_string - ActiveSupport::XmlMini.backend = "REXML" xml_string = "<root></root>".freeze assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) end + + private + def engine + "REXML" + end + + def expansion_attack_error + RuntimeError + end end diff --git a/activesupport/test/xml_mini/xml_mini_engine_test.rb b/activesupport/test/xml_mini/xml_mini_engine_test.rb new file mode 100644 index 0000000000..244e0b0d3a --- /dev/null +++ b/activesupport/test/xml_mini/xml_mini_engine_test.rb @@ -0,0 +1,262 @@ +require "abstract_unit" +require "active_support/xml_mini" +require "active_support/core_ext/hash/conversions" + +class XMLMiniEngineTest < ActiveSupport::TestCase + def self.run_with_gem(gem_name) + require gem_name + yield + rescue LoadError + # Skip tests unless gem is available + end + + def self.run_with_platform(platform_name) + yield if RUBY_PLATFORM.include?(platform_name) + end + + def self.inherited(base) + base.include EngineTests + super + end + + def setup + @default_backend = ActiveSupport::XmlMini.backend + ActiveSupport::XmlMini.backend = engine + super + end + + def teardown + ActiveSupport::XmlMini.backend = @default_backend + super + end + + module EngineTests + def test_file_from_xml + hash = Hash.from_xml(<<-eoxml) + <blog> + <logo type="file" name="logo.png" content_type="image/png"> + </logo> + </blog> + eoxml + assert hash.key?("blog") + assert hash["blog"].key?("logo") + + file = hash["blog"]["logo"] + assert_equal "logo.png", file.original_filename + assert_equal "image/png", file.content_type + end + + def test_exception_thrown_on_expansion_attack + assert_raise expansion_attack_error do + Hash.from_xml(<<-eoxml) + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE member [ + <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> + <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> + <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> + <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> + <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> + <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> + <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> + ]> + <member> + &a; + </member> + eoxml + end + end + + def test_setting_backend + assert_engine_class ActiveSupport::XmlMini.backend + end + + def test_blank_returns_empty_hash + assert_equal({}, ActiveSupport::XmlMini.parse(nil)) + assert_equal({}, ActiveSupport::XmlMini.parse("")) + end + + def test_parse_from_frozen_string + xml_string = "<root/>".freeze + assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) + end + + def test_array_type_makes_an_array + assert_equal_rexml(<<-eoxml) + <blog> + <posts type="array"> + <post>a post</post> + <post>another post</post> + </posts> + </blog> + eoxml + end + + def test_one_node_document_as_hash + assert_equal_rexml(<<-eoxml) + <products/> + eoxml + end + + def test_one_node_with_attributes_document_as_hash + assert_equal_rexml(<<-eoxml) + <products foo="bar"/> + eoxml + end + + def test_products_node_with_book_node_as_hash + assert_equal_rexml(<<-eoxml) + <products> + <book name="awesome" id="12345" /> + </products> + eoxml + end + + def test_products_node_with_two_book_nodes_as_hash + assert_equal_rexml(<<-eoxml) + <products> + <book name="awesome" id="12345" /> + <book name="america" id="67890" /> + </products> + eoxml + end + + def test_single_node_with_content_as_hash + assert_equal_rexml(<<-eoxml) + <products> + hello world + </products> + eoxml + end + + def test_children_with_children + assert_equal_rexml(<<-eoxml) + <root> + <products> + <book name="america" id="67890" /> + </products> + </root> + eoxml + end + + def test_children_with_text + assert_equal_rexml(<<-eoxml) + <root> + <products> + hello everyone + </products> + </root> + eoxml + end + + def test_children_with_non_adjacent_text + assert_equal_rexml(<<-eoxml) + <root> + good + <products> + hello everyone + </products> + morning + </root> + eoxml + end + + def test_parse_from_io + skip_unless_extended_engine + + assert_equal_rexml(StringIO.new(<<-eoxml)) + <root> + good + <products> + hello everyone + </products> + morning + </root> + eoxml + end + + def test_children_with_simple_cdata + skip_unless_extended_engine + + assert_equal_rexml(<<-eoxml) + <root> + <products> + <![CDATA[cdatablock]]> + </products> + </root> + eoxml + end + + def test_children_with_multiple_cdata + skip_unless_extended_engine + + assert_equal_rexml(<<-eoxml) + <root> + <products> + <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]> + </products> + </root> + eoxml + end + + def test_children_with_text_and_cdata + skip_unless_extended_engine + + assert_equal_rexml(<<-eoxml) + <root> + <products> + hello <![CDATA[cdatablock]]> + morning + </products> + </root> + eoxml + end + + def test_children_with_blank_text + skip_unless_extended_engine + + assert_equal_rexml(<<-eoxml) + <root> + <products> </products> + </root> + eoxml + end + + def test_children_with_blank_text_and_attribute + skip_unless_extended_engine + + assert_equal_rexml(<<-eoxml) + <root> + <products type="file"> </products> + </root> + eoxml + end + + private + def engine + raise NotImplementedError + end + + def assert_engine_class(actual) + assert_equal ActiveSupport.const_get("XmlMini_#{engine}"), actual + end + + def assert_equal_rexml(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) + xml.rewind if xml.respond_to?(:rewind) + hash = ActiveSupport::XmlMini.with_backend("REXML") { ActiveSupport::XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end + + def expansion_attack_error + raise NotImplementedError + end + + def extended_engine? + true + end + + def skip_unless_extended_engine + skip "#{engine} is not an extended engine" unless extended_engine? + end + end +end diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index b15ccfb764..b8caa1d74c 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -93,57 +93,57 @@ module XmlMiniTest test "#to_tag should use the type value in the options hash" do @xml.to_tag(:b, "blue", @options.merge(type: "color")) - assert_xml( "<b type=\"color\">blue</b>" ) + assert_xml("<b type=\"color\">blue</b>") end test "#to_tag accepts symbol types" do @xml.to_tag(:b, :name, @options) - assert_xml( "<b type=\"symbol\">name</b>" ) + assert_xml("<b type=\"symbol\">name</b>") end test "#to_tag accepts boolean types" do @xml.to_tag(:b, true, @options) - assert_xml( "<b type=\"boolean\">true</b>") + assert_xml("<b type=\"boolean\">true</b>") end test "#to_tag accepts float types" do @xml.to_tag(:b, 3.14, @options) - assert_xml( "<b type=\"float\">3.14</b>") + assert_xml("<b type=\"float\">3.14</b>") end test "#to_tag accepts decimal types" do @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options) - assert_xml( "<b type=\"decimal\">1.2</b>") + assert_xml("<b type=\"decimal\">1.2</b>") end test "#to_tag accepts date types" do - @xml.to_tag(:b, Date.new(2001,2,3), @options) - assert_xml( "<b type=\"date\">2001-02-03</b>") + @xml.to_tag(:b, Date.new(2001, 2, 3), @options) + assert_xml("<b type=\"date\">2001-02-03</b>") end test "#to_tag accepts datetime types" do - @xml.to_tag(:b, DateTime.new(2001,2,3,4,5,6,"+7"), @options) - assert_xml( "<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>") + @xml.to_tag(:b, DateTime.new(2001, 2, 3, 4, 5, 6, "+7"), @options) + assert_xml("<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>") end test "#to_tag accepts time types" do @xml.to_tag(:b, Time.new(1993, 02, 24, 12, 0, 0, "+09:00"), @options) - assert_xml( "<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>") + assert_xml("<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>") end test "#to_tag accepts array types" do @xml.to_tag(:b, ["first_name", "last_name"], @options) - assert_xml( "<b type=\"array\"><b>first_name</b><b>last_name</b></b>" ) + assert_xml("<b type=\"array\"><b>first_name</b><b>last_name</b></b>") end test "#to_tag accepts hash types" do @xml.to_tag(:b, { first_name: "Bob", last_name: "Marley" }, @options) - assert_xml( "<b><first-name>Bob</first-name><last-name>Marley</last-name></b>" ) + assert_xml("<b><first-name>Bob</first-name><last-name>Marley</last-name></b>") end test "#to_tag should not add type when skip types option is set" do @xml.to_tag(:b, "Bob", @options.merge(skip_types: 1)) - assert_xml( "<b>Bob</b>" ) + assert_xml("<b>Bob</b>") end test "#to_tag should dasherize the space when passed a string with spaces as a key" do @@ -237,22 +237,22 @@ module XmlMiniTest assert_equal :symbol, parser.call("symbol") assert_equal :symbol, parser.call(:symbol) assert_equal :'123', parser.call(123) - assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) } end def test_date parser = @parsing["date"] - assert_equal Date.new(2013,11,12), parser.call("2013-11-12T0211Z") + assert_equal Date.new(2013, 11, 12), parser.call("2013-11-12T0211Z") assert_raises(TypeError) { parser.call(1384190018) } assert_raises(ArgumentError) { parser.call("not really a date") } end def test_datetime parser = @parsing["datetime"] - assert_equal Time.new(2013,11,12,02,11,00,0), parser.call("2013-11-12T02:11:00Z") - assert_equal DateTime.new(2013,11,12), parser.call("2013-11-12T0211Z") - assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T02:11Z") - assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T11:11+9") + assert_equal Time.new(2013, 11, 12, 02, 11, 00, 0), parser.call("2013-11-12T02:11:00Z") + assert_equal DateTime.new(2013, 11, 12), parser.call("2013-11-12T0211Z") + assert_equal DateTime.new(2013, 11, 12, 02, 11), parser.call("2013-11-12T02:11Z") + assert_equal DateTime.new(2013, 11, 12, 02, 11), parser.call("2013-11-12T11:11+9") assert_raises(ArgumentError) { parser.call("1384190018") } end @@ -262,7 +262,7 @@ module XmlMiniTest assert_equal 123, parser.call(123.003) assert_equal 123, parser.call("123") assert_equal 0, parser.call("") - assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) } end def test_float @@ -273,7 +273,7 @@ module XmlMiniTest assert_equal 0.0, parser.call("") assert_equal 123, parser.call(123) assert_equal 123.05, parser.call(123.05) - assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) } end def test_decimal @@ -284,7 +284,7 @@ module XmlMiniTest assert_equal 0.0, parser.call("") assert_equal 123, parser.call(123) assert_raises(ArgumentError) { parser.call(123.04) } - assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) } end def test_boolean @@ -305,7 +305,7 @@ module XmlMiniTest assert_equal "[]", parser.call("[]") assert_equal "[]", parser.call([]) assert_equal "{}", parser.call({}) - assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) } end def test_yaml @@ -316,14 +316,14 @@ product: description : Basketball YAML expected = { - "product"=> [ - { "sku"=>"BL394D", "quantity"=>4, "description"=>"Basketball" } + "product" => [ + { "sku" => "BL394D", "quantity" => 4, "description" => "Basketball" } ] } parser = @parsing["yaml"] assert_equal(expected, parser.call(yaml)) assert_equal({ 1 => "test" }, parser.call(1 => "test")) - assert_equal({ "1 => 'test'"=>nil }, parser.call("{1 => 'test'}")) + assert_equal({ "1 => 'test'" => nil }, parser.call("{1 => 'test'}")) end def test_base64Binary_and_binary @@ -342,11 +342,11 @@ vehemence of any carnal pleasure. EXPECTED parser = @parsing["base64Binary"] - assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64) + assert_equal expected_base64.gsub(/\n/, " ").strip, parser.call(base64) parser.call("NON BASE64 INPUT") parser = @parsing["binary"] - assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64, "encoding" => "base64") + assert_equal expected_base64.gsub(/\n/, " ").strip, parser.call(base64, "encoding" => "base64") assert_equal "IGNORED INPUT", parser.call("IGNORED INPUT", {}) end end diff --git a/ci/phantomjs.js b/ci/phantomjs.js new file mode 100644 index 0000000000..7a33fb14a3 --- /dev/null +++ b/ci/phantomjs.js @@ -0,0 +1,149 @@ +/* + * PhantomJS Runner QUnit Plugin 1.2.0 + * + * PhantomJS binaries: http://phantomjs.org/download.html + * Requires PhantomJS 1.6+ (1.7+ recommended) + * + * Run with: + * phantomjs runner.js [url-of-your-qunit-testsuite] + * + * e.g. + * phantomjs runner.js http://localhost/qunit/test/index.html + */ + +/*global phantom:false, require:false, console:false, window:false, QUnit:false */ + +(function() { + 'use strict'; + + var url, page, timeout, + args = require('system').args; + + // arg[0]: scriptName, args[1...]: arguments + if (args.length < 2 || args.length > 3) { + console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); + phantom.exit(1); + } + + url = args[1]; + page = require('webpage').create(); + if (args[2] !== undefined) { + timeout = parseInt(args[2], 10); + } + + // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) + page.onConsoleMessage = function(msg) { + console.log(msg); + }; + + page.onInitialized = function() { + page.evaluate(addLogging); + }; + + page.onCallback = function(message) { + var result, + failed; + + if (message) { + if (message.name === 'QUnit.done') { + result = message.data; + failed = !result || !result.total || result.failed; + + if (!result.total) { + console.error('No tests were executed. Are you loading tests asynchronously?'); + } + + phantom.exit(failed ? 1 : 0); + } + } + }; + + page.open(url, function(status) { + if (status !== 'success') { + console.error('Unable to access network: ' + status); + phantom.exit(1); + } else { + // Cannot do this verification with the 'DOMContentLoaded' handler because it + // will be too late to attach it if a page does not have any script tags. + var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); + if (qunitMissing) { + console.error('The `QUnit` object is not present on this page.'); + phantom.exit(1); + } + + // Set a timeout on the test running, otherwise tests with async problems will hang forever + if (typeof timeout === 'number') { + setTimeout(function() { + console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); + phantom.exit(1); + }, timeout * 1000); + } + + // Do nothing... the callback mechanism will handle everything! + } + }); + + function addLogging() { + window.document.addEventListener('DOMContentLoaded', function() { + var currentTestAssertions = []; + + QUnit.log(function(details) { + var response; + + // Ignore passing assertions + if (details.result) { + return; + } + + response = details.message || ''; + + if (typeof details.expected !== 'undefined') { + if (response) { + response += ', '; + } + + response += 'expected: ' + details.expected + ', but was: ' + details.actual; + } + + if (details.source) { + response += "\n" + details.source; + } + + currentTestAssertions.push('Failed assertion: ' + response); + }); + + QUnit.testDone(function(result) { + var i, + len, + name = ''; + + if (result.module) { + name += result.module + ': '; + } + name += result.name; + + if (result.failed) { + console.log('\n' + 'Test failed: ' + name); + + for (i = 0, len = currentTestAssertions.length; i < len; i++) { + console.log(' ' + currentTestAssertions[i]); + } + } + + currentTestAssertions.length = 0; + }); + + QUnit.done(function(result) { + console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); + + if (typeof window.callPhantom === 'function') { + window.callPhantom({ + 'name': 'QUnit.done', + 'data': result + }); + } + }); + }, false); + } +})(); + diff --git a/ci/travis.rb b/ci/travis.rb index 1d658bae47..bb87c8f4ad 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -36,8 +36,10 @@ class Build def run!(options = {}) self.options.update(options) + Dir.chdir(dir) do announce(heading) + if guides? run_bug_report_templates else @@ -69,7 +71,7 @@ class Build end tasks else - ["test", ("isolated" if isolated?), ("integration" if integration?)].compact.join(":") + ["test", ("isolated" if isolated?), ("integration" if integration?), ("ujs" if ujs?)].compact.join(":") end end @@ -92,6 +94,10 @@ class Build gem == "guides" end + def ujs? + component.split(":").last == "ujs" + end + def isolated? options[:isolated] end @@ -136,7 +142,7 @@ class Build end end -if ENV["GEM"]=="aj:integration" +if ENV["GEM"] == "aj:integration" ENV["QC_DATABASE_URL"] = "postgres://postgres@localhost/active_jobs_qc_int_test" ENV["QUE_DATABASE_URL"] = "postgres://postgres@localhost/active_jobs_que_int_test" end @@ -146,15 +152,16 @@ 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 gem == "railties" && isolated next if gem == "ac" && isolated next if gem == "ac:integration" && isolated next if gem == "aj:integration" && isolated next if gem == "guides" && isolated + next if gem == "av:ujs" && isolated build = Build.new(gem, isolated: isolated) results[build.key] = build.run! - end end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 2730d2dfea..d8b122d264 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,2 +1 @@ - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/guides/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/guides/CHANGELOG.md) for previous changes. diff --git a/guides/Rakefile b/guides/Rakefile index 353966fc55..3a6f10040f 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -1,23 +1,35 @@ namespace :guides do - desc 'Generate guides (for authors), use ONLY=foo to process just "foo.md"' task generate: "generate:html" - namespace :generate do + # Guides are written in UTF-8, but the environment may be configured for some + # other locale, these tasks are responsible for ensuring the default external + # encoding is UTF-8. + # + # Real use cases: Generation was reported to fail on a machine configured with + # GBK (Chinese). The docs server once got misconfigured somehow and had "C", + # which broke generation too. + task :encoding do + %w(LANG LANGUAGE LC_ALL).each do |env_var| + ENV[env_var] = "en_US.UTF-8" + end + end + namespace :generate do desc "Generate HTML guides" - task :html do + task html: :encoding do ENV["WARNINGS"] = "1" # authors can't disable this ruby "rails_guides.rb" end desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/gp/feature.html?docId=1000765211" - task :kindle do - unless `kindlerb -v 2> /dev/null` =~ /kindlerb 0.1.1/ - abort "Please `gem install kindlerb` and make sure you have `kindlegen` in your PATH" + task kindle: :encoding do + require "kindlerb" + unless Kindlerb.kindlegen_available? + abort "Please run `setupkindlerb` to install kindlegen" end unless `convert` =~ /convert/ - abort "Please install ImageMagick`" + abort "Please install ImageMagick" end ENV["KINDLE"] = "1" Rake::Task["guides:generate:html"].invoke @@ -26,13 +38,13 @@ namespace :guides do # Validate guides ------------------------------------------------------------------------- desc 'Validate guides, use ONLY=foo to process just "foo.html"' - task :validate do + task validate: :encoding do ruby "w3c_validator.rb" end desc "Show help" task :help do - puts <<-help + puts <<HELP Guides are taken from the source directory, and the result goes into the output directory. Assets are stored under files, and copied to output/files as @@ -45,8 +57,9 @@ All of these processes are handled via rake tasks, here's a full list of them: #{%x[rake -T]} Some arguments may be passed via environment variables: - WARNINGS=1 - Internal links (anchors) are checked, also detects duplicated IDs. + RAILS_VERSION=tag + If guides are being generated for a specific Rails version set the Git tag + here, otherwise the current SHA1 is going to be used to generate edge guides. ALL=1 Force generation of all guides. @@ -64,15 +77,12 @@ Some arguments may be passed via environment variables: Use it when you want to generate translated guides in source/<GUIDES_LANGUAGE> folder (such as source/es) - EDGE=1 - Indicate generated guides should be marked as edge. - Examples: - $ rake guides:generate ALL=1 - $ rake guides:generate EDGE=1 - $ rake guides:generate:kindle EDGE=1 + $ rake guides:generate ALL=1 RAILS_VERSION=v5.1.0 + $ rake guides:generate ONLY=migrations + $ rake guides:generate:kindle $ rake guides:generate GUIDES_LANGUAGE=es - help +HELP end end diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index b56699a0d0..b27776745a 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -294,6 +294,10 @@ a, a:link, a:visited { #mainCol a, #subCol a, #feature a {color: #980905;} #mainCol a code, #subCol a code, #feature a code {color: #980905;} +#mainCol a.anchorlink, #mainCol a.anchorlink code {color: #333;} +#mainCol a.anchorlink { text-decoration: none; } +#mainCol a.anchorlink:hover { text-decoration: underline; } + /* Navigation --------------------------------------- */ diff --git a/guides/assets/stylesheets/responsive-tables.css b/guides/assets/stylesheets/responsive-tables.css index f5fbcbf948..f5fbcbf948 100755..100644 --- a/guides/assets/stylesheets/responsive-tables.css +++ b/guides/assets/stylesheets/responsive-tables.css diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 960d269d90..8b7aa893fd 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -8,14 +8,14 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "rails", "5.0.0" + gem "rails", "5.1.0" end require "rack/test" require "action_controller/railtie" class TestApp < Rails::Application - config.root = File.dirname(__FILE__) + config.root = __dir__ config.session_store :cookie_store, key: "cookie_store_key" secrets.secret_token = "secret_token" secrets.secret_key_base = "secret_key_base" diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 486c7243ad..3dd66c95ec 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -8,12 +8,13 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" + gem "arel", github: "rails/arel" end require "action_controller/railtie" class TestApp < Rails::Application - config.root = File.dirname(__FILE__) + config.root = __dir__ secrets.secret_token = "secret_token" secrets.secret_key_base = "secret_key_base" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index debc46ad54..252b270a0c 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activejob", "5.0.0" + gem "activejob", "5.1.0" end require "minitest/autorun" diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb index f61518713f..7591470440 100644 --- a/guides/bug_report_templates/active_job_master.rb +++ b/guides/bug_report_templates/active_job_master.rb @@ -8,6 +8,7 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" + gem "arel", github: "rails/arel" end require "active_job" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index e18302fe65..61d4e8d395 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.0.0" + gem "activerecord", "5.1.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 7265a671b0..8bbc1ef19e 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -8,6 +8,7 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" + gem "arel", github: "rails/arel" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index f568a111f6..0c398e334a 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.0.0.1" + gem "activerecord", "5.1.0" gem "sqlite3" end @@ -62,4 +62,4 @@ class BugTest < Minitest::Test assert_equal "decimal(10,0)", Payment.columns.last.sql_type end -end
\ No newline at end of file +end diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb index ef7b42e0a6..84a4b71909 100644 --- a/guides/bug_report_templates/active_record_migrations_master.rb +++ b/guides/bug_report_templates/active_record_migrations_master.rb @@ -8,6 +8,7 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" + gem "arel", github: "rails/arel" gem "sqlite3" end @@ -61,4 +62,4 @@ class BugTest < Minitest::Test assert_equal "decimal(10,0)", Payment.columns.last.sql_type end -end
\ No newline at end of file +end diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb new file mode 100644 index 0000000000..54433b34dd --- /dev/null +++ b/guides/bug_report_templates/benchmark.rb @@ -0,0 +1,49 @@ +begin + require "bundler/inline" +rescue LoadError => e + $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" + raise e +end + +gemfile(true) do + source "https://rubygems.org" + gem "rails", github: "rails/rails" + gem "benchmark-ips" +end + +require "active_support" +require "active_support/core_ext/object/blank" + +# Your patch goes here. +class String + def fast_blank? + true + end +end + +# Enumerate some representative scenarios here. +# +# It is very easy to make an optimization that improves performance for a +# specific scenario you care about but regresses on other common cases. +# Therefore, you should test your change against a list of representative +# scenarios. Ideally, they should be based on real-world scenarios extracted +# from production applications. +SCENARIOS = { + "Empty" => "", + "Single Space" => " ", + "Two Spaces" => " ", + "Mixed Whitspaces" => " \t\r\n", + "Very Long String" => " " * 100 +} + +SCENARIOS.each_pair do |name, value| + puts + puts " #{name} ".center(80, "=") + puts + + Benchmark.ips do |x| + x.report("blank?") { value.blank? } + x.report("fast_blank?") { value.fast_blank? } + x.compare! + end +end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index a94848e25b..4dcd04ea27 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activesupport", "5.0.0" + gem "activesupport", "5.1.0" end require "active_support/core_ext/object/blank" diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index d3a7ae4ac4..ed45726e92 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -8,6 +8,7 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" + gem "arel", github: "rails/arel" end require "active_support" diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index 557d23f78c..0f611c8f2b 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -1,17 +1,27 @@ -pwd = File.dirname(__FILE__) -$:.unshift pwd +$:.unshift __dir__ -begin - # Guides generation in the Rails repo. - as_lib = File.join(pwd, "../activesupport/lib") - ap_lib = File.join(pwd, "../actionpack/lib") +as_lib = File.expand_path("../activesupport/lib", __dir__) +ap_lib = File.expand_path("../actionpack/lib", __dir__) +av_lib = File.expand_path("../actionview/lib", __dir__) - $:.unshift as_lib if File.directory?(as_lib) - $:.unshift ap_lib if File.directory?(ap_lib) -rescue LoadError - # Guides generation from gems. - gem "actionpack", ">= 3.0" -end +$:.unshift as_lib if File.directory?(as_lib) +$:.unshift ap_lib if File.directory?(ap_lib) +$:.unshift av_lib if File.directory?(av_lib) require "rails_guides/generator" -RailsGuides::Generator.new.generate +require "active_support/core_ext/object/blank" + +env_value = ->(name) { ENV[name].presence } +env_flag = ->(name) { "1" == env_value[name] } + +version = env_value["RAILS_VERSION"] +edge = `git rev-parse HEAD`.strip unless version + +RailsGuides::Generator.new( + edge: edge, + version: version, + all: env_flag["ALL"], + only: env_value["ONLY"], + kindle: env_flag["KINDLE"], + language: env_value["GUIDES_LANGUAGE"] +).generate diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index 39a57191eb..35f014747c 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -1,54 +1,3 @@ -# --------------------------------------------------------------------------- -# -# This script generates the guides. It can be invoked via the -# guides:generate rake task within the guides directory. -# -# Guides are taken from the source directory, and the resulting HTML goes into the -# output directory. Assets are stored under files, and copied to output/files as -# part of the generation process. -# -# Some arguments may be passed via environment variables: -# -# WARNINGS -# If you are writing a guide, please work always with WARNINGS=1. Users can -# generate the guides, and thus this flag is off by default. -# -# Internal links (anchors) are checked. If a reference is broken levenshtein -# distance is used to suggest an existing one. This is useful since IDs are -# generated by Markdown from headers and thus edits alter them. -# -# Also detects duplicated IDs. They happen if there are headers with the same -# text. Please do resolve them, if any, so guides are valid XHTML. -# -# ALL -# Set to "1" to force the generation of all guides. -# -# ONLY -# Use ONLY if you want to generate only one or a set of guides. Prefixes are -# enough: -# -# # generates only association_basics.html -# ONLY=assoc rake guides:generate -# -# Separate many using commas: -# -# # generates only association_basics.html and command_line.html -# ONLY=assoc,command rake guides:generate -# -# Note that if you are working on a guide generation will by default process -# only that one, so ONLY is rarely used nowadays. -# -# GUIDES_LANGUAGE -# Use GUIDES_LANGUAGE when you want to generate translated guides in -# <tt>source/<GUIDES_LANGUAGE></tt> folder (such as <tt>source/es</tt>). -# Ignore it when generating English guides. -# -# EDGE -# Set to "1" to indicate generated guides should be marked as edge. This -# inserts a badge and changes the preamble of the home page. -# -# --------------------------------------------------------------------------- - require "set" require "fileutils" @@ -64,46 +13,37 @@ require "rails_guides/levenshtein" module RailsGuides class Generator - attr_reader :guides_dir, :source_dir, :output_dir, :edge, :warnings, :all - GUIDES_RE = /\.(?:erb|md)\z/ - def initialize(output=nil) - set_flags_from_environment + def initialize(edge:, version:, all:, only:, kindle:, language:) + @edge = edge + @version = version + @all = all + @only = only + @kindle = kindle + @language = language - if kindle? + if @kindle check_for_kindlegen register_kindle_mime_types end - initialize_dirs(output) + initialize_dirs create_output_dir_if_needed - end - - def set_flags_from_environment - @edge = ENV["EDGE"] == "1" - @warnings = ENV["WARNINGS"] == "1" - @all = ENV["ALL"] == "1" - @kindle = ENV["KINDLE"] == "1" - @version = ENV["RAILS_VERSION"] || "local" - @lang = ENV["GUIDES_LANGUAGE"] - end - - def register_kindle_mime_types - Mime::Type.register_alias("application/xml", :opf, %w(opf)) - Mime::Type.register_alias("application/xml", :ncx, %w(ncx)) + initialize_markdown_renderer end def generate generate_guides copy_assets - generate_mobi if kindle? + generate_mobi if @kindle end private - def kindle? - @kindle + def register_kindle_mime_types + Mime::Type.register_alias("application/xml", :opf, %w(opf)) + Mime::Type.register_alias("application/xml", :ncx, %w(ncx)) end def check_for_kindlegen @@ -114,29 +54,35 @@ module RailsGuides def generate_mobi require "rails_guides/kindle" - out = "#{output_dir}/kindlegen.out" - Kindle.generate(output_dir, mobi, out) + out = "#{@output_dir}/kindlegen.out" + Kindle.generate(@output_dir, mobi, out) puts "(kindlegen log at #{out})." end def mobi - "ruby_on_rails_guides_#@version%s.mobi" % (@lang.present? ? ".#@lang" : "") + mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}" + mobi += ".#{@language}" if @language + mobi += ".mobi" end - def initialize_dirs(output) - @guides_dir = File.join(File.dirname(__FILE__), "..") - @source_dir = "#@guides_dir/source/#@lang" - @output_dir = if output - output - elsif kindle? - "#@guides_dir/output/kindle/#@lang" - else - "#@guides_dir/output/#@lang" - end.sub(%r</$>, "") + def initialize_dirs + @guides_dir = File.expand_path("..", __dir__) + + @source_dir = "#{@guides_dir}/source" + @source_dir += "/#{@language}" if @language + + @output_dir = "#{@guides_dir}/output" + @output_dir += "/kindle" if @kindle + @output_dir += "/#{@language}" if @language end def create_output_dir_if_needed - FileUtils.mkdir_p(output_dir) + FileUtils.mkdir_p(@output_dir) + end + + def initialize_markdown_renderer + Markdown::Renderer.edge = @edge + Markdown::Renderer.version = @version end def generate_guides @@ -147,27 +93,27 @@ module RailsGuides end def guides_to_generate - guides = Dir.entries(source_dir).grep(GUIDES_RE) + guides = Dir.entries(@source_dir).grep(GUIDES_RE) - if kindle? - Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry| + if @kindle + Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry| next if entry == "KINDLE.md" guides << "kindle/#{entry}" end end - ENV.key?("ONLY") ? select_only(guides) : guides + @only ? select_only(guides) : guides end def select_only(guides) - prefixes = ENV["ONLY"].split(",").map(&:strip) + prefixes = @only.split(",").map(&:strip) guides.select do |guide| - guide.start_with?("kindle".freeze, *prefixes) + guide.start_with?("kindle", *prefixes) end end def copy_assets - FileUtils.cp_r(Dir.glob("#{guides_dir}/assets/*"), output_dir) + FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir) end def output_file_for(guide) @@ -179,22 +125,28 @@ module RailsGuides end def output_path_for(output_file) - File.join(output_dir, File.basename(output_file)) + File.join(@output_dir, File.basename(output_file)) end def generate?(source_file, output_file) - fin = File.join(source_dir, source_file) + fin = File.join(@source_dir, source_file) fout = output_path_for(output_file) - all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) + @all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) end def generate_guide(guide, output_file) output_path = output_path_for(output_file) puts "Generating #{guide} as #{output_file}" - layout = kindle? ? "kindle/layout" : "layout" + layout = @kindle ? "kindle/layout" : "layout" File.open(output_path, "w") do |f| - view = ActionView::Base.new(source_dir, edge: @edge, version: @version, mobi: "kindle/#{mobi}", lang: @lang) + view = ActionView::Base.new( + @source_dir, + edge: @edge, + version: @version, + mobi: "kindle/#{mobi}", + language: @language + ) view.extend(Helpers) if guide =~ /\.(\w+)\.erb$/ @@ -202,10 +154,15 @@ module RailsGuides # Passing a template handler in the template name is deprecated. So pass the file name without the extension. result = view.render(layout: layout, formats: [$1], file: $`) else - body = File.read(File.join(source_dir, guide)) - result = RailsGuides::Markdown.new(view, layout).render(body) - - warn_about_broken_links(result) if @warnings + body = File.read("#{@source_dir}/#{guide}") + result = RailsGuides::Markdown.new( + view: view, + layout: layout, + edge: @edge, + version: @version + ).render(body) + + warn_about_broken_links(result) end f.write(result) @@ -231,7 +188,7 @@ module RailsGuides # Footnotes. anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten) anchors += Set.new(html.scan(/<sup\s+class="footnote"\s+id="([^"]+)/).flatten) - return anchors + anchors end def check_fragment_identifiers(html, anchors) diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index 888b51b1ef..520aa7f7cc 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -15,7 +15,7 @@ module RailsGuides end def documents_by_section - @documents_by_section ||= YAML.load_file(File.expand_path("../../source/#{@lang ? @lang + '/' : ''}documents.yaml", __FILE__)) + @documents_by_section ||= YAML.load_file(File.expand_path("../source/#{@language ? @language + '/' : ''}documents.yaml", __dir__)) end def documents_flat @@ -26,7 +26,7 @@ module RailsGuides documents.reject { |document| document["work_in_progress"] } end - def docs_for_menu(position=nil) + def docs_for_menu(position = nil) if position.nil? documents_by_section elsif position == "L" diff --git a/guides/rails_guides/indexer.rb b/guides/rails_guides/indexer.rb index 56df6a5842..c58b6b85a2 100644 --- a/guides/rails_guides/indexer.rb +++ b/guides/rails_guides/indexer.rb @@ -17,7 +17,7 @@ module RailsGuides private - def process(string, current_level=3, counters=[1]) + def process(string, current_level = 3, counters = [1]) s = StringScanner.new(string) level_hash = {} diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb index 6fb8183cb1..9536d0bd3b 100644 --- a/guides/rails_guides/kindle.rb +++ b/guides/rails_guides/kindle.rb @@ -1,9 +1,6 @@ #!/usr/bin/env ruby -unless `which kindlerb` - abort "Please gem install kindlerb" -end - +require "kindlerb" require "nokogiri" require "fileutils" require "yaml" @@ -28,10 +25,9 @@ module Kindle generate_document_metadata(mobi_outfile) puts "Creating MOBI document with kindlegen. This may take a while." - cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1" - puts cmd - system(cmd) - puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}" + if Kindlerb.run(output_dir) + puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}" + end end end @@ -56,7 +52,7 @@ module Kindle h2["id"] = h2.inner_text.gsub(/\s/, "-") end add_head_section fdoc, "Front Matter" - File.open("frontmatter.html","w") { |f| f.puts fdoc.to_html } + File.open("frontmatter.html", "w") { |f| f.puts fdoc.to_html } html_pages.unshift "frontmatter.html" end @@ -68,7 +64,7 @@ module Kindle title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", "") title = page.capitalize.gsub(".html", "") if title.strip == "" File.open("sections/%03d/_section.txt" % section_idx, "w") { |f| f.puts title } - doc.xpath("//h3[@id]").each_with_index do |h3,item_idx| + doc.xpath("//h3[@id]").each_with_index do |h3, item_idx| subsection = h3.inner_text content = h3.xpath("./following-sibling::*").take_while { |x| x.name != "h3" }.map(&:to_html) item = Nokogiri::HTML(h3.to_html + content.join("\n")) diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index e947150364..40c6a5c372 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -20,12 +20,12 @@ module RailsGuides str2_codepoint_enumerable = str2.each_codepoint str1.each_codepoint.with_index do |char1, i| - e = i+1 + e = i + 1 str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ - d[j+1] + 1, # insertion + d[j + 1] + 1, # insertion e + 1, # deletion d[j] + cost # substitution ].min diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 33563d669c..02d58601c4 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -4,12 +4,14 @@ require "rails_guides/markdown/renderer" module RailsGuides class Markdown - def initialize(view, layout) - @view = view - @layout = layout + def initialize(view:, layout:, edge:, version:) + @view = view + @layout = layout + @edge = edge + @version = version @index_counter = Hash.new(0) - @raw_header = "" - @node_ids = {} + @raw_header = "" + @node_ids = {} end def render(body) @@ -60,7 +62,8 @@ module RailsGuides autolink: true, strikethrough: true, superscript: true, - tables: true) + tables: true + ) end def extract_raw_header_and_body @@ -102,6 +105,10 @@ module RailsGuides node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}" end end + + doc.css("h3, h4, h5, h6").each do |node| + node.inner_html = "<a class='anchorlink' href='##{node[:id]}'>#{node.inner_html}</a>" + end end.to_html end end @@ -158,7 +165,7 @@ module RailsGuides @view.content_for(:header_section) { @header } @view.content_for(:page_title) { @title } @view.content_for(:index_section) { @index } - @view.render(layout: @layout, text: @body) + @view.render(layout: @layout, html: @body.html_safe) end end end diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index f8e32fc498..7ac3d417a4 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -1,9 +1,7 @@ module RailsGuides class Markdown class Renderer < Redcarpet::Render::HTML - def initialize(options={}) - super - end + cattr_accessor :edge, :version def block_code(code, language) <<-HTML @@ -15,6 +13,16 @@ module RailsGuides HTML end + def link(url, title, content) + if url.start_with?("http://api.rubyonrails.org") + %(<a href="#{api_link(url)}">#{content}</a>) + elsif title + %(<a href="#{url}" title="#{title}">#{content}</a>) + else + %(<a href="#{url}">#{content}</a>) + end + end + def header(text, header_level) # Always increase the heading level by 1, so we can use h1, h2 heading in the document header_level += 1 @@ -23,7 +31,9 @@ HTML end def paragraph(text) - if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ + if text =~ %r{^NOTE:\s+Defined\s+in\s+<code>(.*?)</code>\.?$} + %(<div class="note"><p>Defined in <code><a href="#{github_file_url($1)}">#{$1}</a></code>.</p></div>) + elsif text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ convert_notes(text) elsif text.include?("DO NOT READ THIS FILE ON GITHUB") elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/ @@ -52,7 +62,7 @@ HTML "ruby; html-script: true" when "html" "xml" # HTML is understood, but there are .xml rules in the CSS - else + else "plain" end end @@ -79,6 +89,33 @@ HTML %(<div class="#{css_class}"><p>#{$2.strip}</p></div>) end end + + def github_file_url(file_path) + tree = version || edge + + root = file_path[%r{(\w+)/}, 1] + path = \ + case root + when "abstract_controller", "action_controller", "action_dispatch" + "actionpack/lib/#{file_path}" + when /\A(action|active)_/ + "#{root.sub("_", "")}/lib/#{file_path}" + else + file_path + end + + "https://github.com/rails/rails/tree/#{tree}/#{path}" + end + + def api_link(url) + if url =~ %r{http://api\.rubyonrails\.org/v\d+\.} + url + elsif edge + url.sub("api", "edgeapi") + else + url.sub(/(?<=\.org)/, "/#{version}") + end + end end end end diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index c6bac34d18..ac5833e069 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -45,7 +45,6 @@ The internal documentation of Rails, in the form of code comments, has been impr * [A Guide to Testing Rails Applications](testing.html) * [Securing Rails Applications](security.html) * [Debugging Rails Applications](debugging_rails_applications.html) -* [Performance Testing Rails Applications](performance_testing.html) * [The Basics of Creating Rails Plugins](plugins.html) All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers. diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md index 50886a57a7..5f4be07351 100644 --- a/guides/source/5_0_release_notes.md +++ b/guides/source/5_0_release_notes.md @@ -150,7 +150,7 @@ The type of an attribute is given the opportunity to change how dirty tracking is performed. See its -[documentation](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) +[documentation](http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html) for a detailed write up. @@ -242,7 +242,7 @@ Please refer to the [Changelog][railties] for detailed changes. [Pull Request](https://github.com/rails/rails/pull/22288)) * New applications are generated with the evented file system monitor enabled - on Linux and Mac OS X. The feature can be opted out by passing + on Linux and macOS. The feature can be opted out by passing `--skip-listen` to the generator. ([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003), [commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202)) @@ -417,7 +417,7 @@ Please refer to the [Changelog][action-pack] for detailed changes. `ActionDispatch::IntegrationTest` instead. ([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d)) -* Rails will only generate "weak", instead of strong ETags. +* Rails generates weak ETags by default. ([Pull Request](https://github.com/rails/rails/pull/17573)) * Controller actions without an explicit `render` call and with no @@ -453,6 +453,9 @@ Please refer to the [Changelog][action-pack] for detailed changes. `ActionController::Live`. ([More details in this issue](https://github.com/rails/rails/issues/25581)) +* Introduce `Response#strong_etag=` and `#weak_etag=` and analogous + options for `fresh_when` and `stale?`. + ([Pull Request](https://github.com/rails/rails/pull/24387)) Action View ------------- @@ -496,6 +499,9 @@ Please refer to the [Changelog][action-view] for detailed changes. `datetime-local`. ([Pull Request](https://github.com/rails/rails/pull/25469)) +* Allow blocks while rendering with the `render partial:` helper. + ([Pull Request](https://github.com/rails/rails/pull/17974)) + Action Mailer ------------- @@ -582,8 +588,8 @@ Please refer to the [Changelog][active-record] for detailed changes. gem. ([Pull Request](https://github.com/rails/rails/pull/21161)) * Removed support for the legacy `mysql` database adapter from core. Most users should - be able to use `mysql2`. It will be converted to a separate gem when when we find someone - to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642)], + be able to use `mysql2`. It will be converted to a separate gem when we find someone + to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642), [Pull Request 2](https://github.com/rails/rails/pull/22715)) * Removed support for the `protected_attributes` gem. @@ -1000,7 +1006,8 @@ Please refer to the [Changelog][active-support] for detailed changes. * Added `#on_weekend?`, `#on_weekday?`, `#next_weekday`, `#prev_weekday` methods to `Date`, `Time`, and `DateTime`. - ([Pull Request](https://github.com/rails/rails/pull/18335)) + ([Pull Request](https://github.com/rails/rails/pull/18335), + [Pull Request](https://github.com/rails/rails/pull/23687)) * Added `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, and `DateTime`. @@ -1051,9 +1058,6 @@ Please refer to the [Changelog][active-support] for detailed changes. * Added `Array#second_to_last` and `Array#third_to_last` methods. ([Pull Request](https://github.com/rails/rails/pull/23583)) -* Added `#on_weekday?` method to `Date`, `Time`, and `DateTime`. - ([Pull Request](https://github.com/rails/rails/pull/23687)) - * Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow components and libraries to manage, and participate in, the execution of application code, and the application reloading process. diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md new file mode 100644 index 0000000000..fa92b9e5f8 --- /dev/null +++ b/guides/source/5_1_release_notes.md @@ -0,0 +1,652 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Ruby on Rails 5.1 Release Notes +=============================== + +Highlights in Rails 5.1: + +* Yarn Support +* Optional Webpack support +* jQuery no longer a default dependency +* System tests +* Encrypted secrets +* Parameterized mailers +* Direct & resolved routes +* Unification of form_for and form_tag into form_with + +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-1-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 5.1 +---------------------- + +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.0 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 5.1. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-0-to-rails-5-1) +guide. + + +Major Features +-------------- + +### Yarn Support + +[Pull Request](https://github.com/rails/rails/pull/26836) + +Rails 5.1 allows managing JavaScript dependencies +from NPM via Yarn. This will make it easy to use libraries like React, VueJS +or any other library from NPM world. The Yarn support is integrated with +the asset pipeline so that all dependencies will work seamlessly with the +Rails 5.1 app. + +### Optional Webpack support + +[Pull Request](https://github.com/rails/rails/pull/27288) + +Rails apps can integrate with [Webpack](https://webpack.js.org/), a JavaScript +asset bundler, more easily using the new [Webpacker](https://github.com/rails/webpacker) +gem. Use the `--webpack` flag when generating new applications to enable Webpack +integration. + +This is fully compatible with the asset pipeline, which you can continue to use for +images, fonts, sounds, and other assets. You can even have some JavaScript code +managed by the asset pipeline, and other code processed via Webpack. All of this is managed +by Yarn, which is enabled by default. + +### jQuery no longer a default dependency + +[Pull Request](https://github.com/rails/rails/pull/27113) + +jQuery was required by default in earlier versions of Rails to provide features +like `data-remote`, `data-confirm` and other parts of Rails' Unobtrusive JavaScript +offerings. It is no longer required, as the UJS has been rewritten to use plain, +vanilla JavaScript. This code now ships inside of Action View as +`rails-ujs`. + +You can still use jQuery if needed, but it is no longer required by default. + +### System tests + +[Pull Request](https://github.com/rails/rails/pull/26703) + +Rails 5.1 has baked-in support for writing Capybara tests, in the form of +System tests. You no longer need to worry about configuring Capybara and +database cleaning strategies for such tests. Rails 5.1 provides a wrapper +for running tests in Chrome with additional features such as failure +screenshots. + +### Encrypted secrets + +[Pull Request](https://github.com/rails/rails/pull/28038) + +Rails now allows management of application secrets in a secure way, +inspired by the [sekrets](https://github.com/ahoward/sekrets) gem. + +Run `bin/rails secrets:setup` to setup a new encrypted secrets file. This will +also generate a master key, which must be stored outside of the repository. The +secrets themselves can then be safely checked into the revision control system, +in an encrypted form. + +Secrets will be decrypted in production, using a key stored either in the +`RAILS_MASTER_KEY` environment variable, or in a key file. + +### Parameterized mailers + +[Pull Request](https://github.com/rails/rails/pull/27825) + +Allows specifying common parameters used for all methods in a mailer class in +order to share instance variables, headers and other common setup. + +``` ruby +class InvitationsMailer < ApplicationMailer + before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + before_action { @account = params[:inviter].account } + + def account_invitation + mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + end +end + +InvitationsMailer.with(inviter: person_a, invitee: person_b) + .account_invitation.deliver_later +``` + +### Direct & resolved routes + +[Pull Request](https://github.com/rails/rails/pull/23138) + +Rails 5.1 adds two new methods, `resolve` and `direct`, to the routing +DSL. The `resolve` method allows customizing polymorphic mapping of models. + +``` 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`. + +The `direct` method allows creation of custom URL helpers. + +``` ruby +direct(:homepage) { "http://www.rubyonrails.org" } + +>> 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 +``` + +### Unification of form_for and form_tag into form_with + +[Pull Request](https://github.com/rails/rails/pull/26976) + +Before Rails 5.1, there were two interfaces for handling HTML forms: +`form_for` for model instances and `form_tag` for custom URLs. + +Rails 5.1 combines both of these interfaces with `form_with`, and +can generate form tags based on URLs, scopes or models. + +Using just a URL: + +``` erb +<%= form_with url: posts_path do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +<form action="/posts" method="post" data-remote="true"> + <input type="text" name="title"> +</form> +``` + +Adding a scope prefixes the input field names: + +``` erb +<%= form_with scope: :post, url: posts_path do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +<form action="/posts" method="post" data-remote="true"> + <input type="text" name="post[title]"> +</form> +``` + +Using a model infers both the URL and scope: + +``` erb +<%= form_with model: Post.new do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +<form action="/posts" method="post" data-remote="true"> + <input type="text" name="post[title]"> +</form> +``` + +An existing model makes an update form and fills out field values: + +``` erb +<%= form_with model: Post.first do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +<form action="/posts/1" method="post" data-remote="true"> + <input type="hidden" name="_method" value="patch"> + <input type="text" name="post[title]" value="<the title of the post>"> +</form> +``` + +Incompatibilities +----------------- + +The following changes may require immediate action upon upgrade. + +### Transactional tests with multiple connections + +Transactional tests now wrap all Active Record connections in database +transactions. + +When a test spawns additional threads, and those threads obtain database +connections, those connections are now handled specially: + +The threads will share a single connection, which is inside the managed +transaction. This ensures all threads see the database in the same +state, ignoring the outermost transaction. Previously, such additional +connections were unable to see the fixture rows, for example. + +When a thread enters a nested transaction, it will temporarily obtain +exclusive use of the connection, to maintain isolation. + +If your tests currently rely on obtaining a separate, +outside-of-transaction, connection in a spawned thread, you'll need to +switch to more explicit connection management. + +If your tests spawn threads and those threads interact while also using +explicit database transactions, this change may introduce a deadlock. + +The easy way to opt out of this new behavior is to disable transactional +tests on any test cases it affects. + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +* Remove deprecated `config.static_cache_control`. + ([commit](https://github.com/rails/rails/commit/c861decd44198f8d7d774ee6a74194d1ac1a5a13)) + +* Remove deprecated `config.serve_static_files`. + ([commit](https://github.com/rails/rails/commit/0129ca2eeb6d5b2ea8c6e6be38eeb770fe45f1fa)) + +* Remove deprecated file `rails/rack/debugger`. + ([commit](https://github.com/rails/rails/commit/7563bf7b46e6f04e160d664e284a33052f9804b8)) + +* Remove deprecated tasks: `rails:update`, `rails:template`, `rails:template:copy`, + `rails:update:configs` and `rails:update:bin`. + ([commit](https://github.com/rails/rails/commit/f7782812f7e727178e4a743aa2874c078b722eef)) + +* Remove deprecated `CONTROLLER` environment variable for `routes` task. + ([commit](https://github.com/rails/rails/commit/f9ed83321ac1d1902578a0aacdfe55d3db754219)) + +* Remove -j (--javascript) option from `rails new` command. + ([Pull Request](https://github.com/rails/rails/pull/28546)) + +### Notable changes + +* Added a shared section to `config/secrets.yml` that will be loaded for all + environments. + ([commit](https://github.com/rails/rails/commit/e530534265d2c32b5c5f772e81cb9002dcf5e9cf)) + +* The config file `config/secrets.yml` is now loaded in with all keys as symbols. + ([Pull Request](https://github.com/rails/rails/pull/26929)) + +* Removed jquery-rails from default stack. rails-ujs, which is shipped + with Action View, is included as default UJS adapter. + ([Pull Request](https://github.com/rails/rails/pull/27113)) + +* Add Yarn support in new apps with a yarn binstub and package.json. + ([Pull Request](https://github.com/rails/rails/pull/26836)) + +* Add Webpack support in new apps via the `--webpack` option, which will delegate + to the rails/webpacker gem. + ([Pull Request](https://github.com/rails/rails/pull/27288)) + +* Initialize Git repo when generating new app, if option `--skip-git` is not + provided. + ([Pull Request](https://github.com/rails/rails/pull/27632)) + +* Add encrypted secrets in `config/secrets.yml.enc`. + ([Pull Request](https://github.com/rails/rails/pull/28038)) + +* Display railtie class name in `rails initializers`. + ([Pull Request](https://github.com/rails/rails/pull/25257)) + +Action Cable +----------- + +Please refer to the [Changelog][action-cable] for detailed changes. + +### Notable changes + +* Added support for `channel_prefix` to Redis and evented Redis adapters + in `cable.yml` to avoid name collisions when using the same Redis server + with multiple applications. + ([Pull Request](https://github.com/rails/rails/pull/27425)) + +* Add `ActiveSupport::Notifications` hook for broadcasting data. + ([Pull Request](https://github.com/rails/rails/pull/24988)) + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +* Removed support for non-keyword arguments in `#process`, `#get`, `#post`, + `#patch`, `#put`, `#delete`, and `#head` for the `ActionDispatch::IntegrationTest` + and `ActionController::TestCase` classes. + ([Commit](https://github.com/rails/rails/commit/98b8309569a326910a723f521911e54994b112fb), + [Commit](https://github.com/rails/rails/commit/de9542acd56f60d281465a59eac11e15ca8b3323)) + +* Removed deprecated `ActionDispatch::Callbacks.to_prepare` and + `ActionDispatch::Callbacks.to_cleanup`. + ([Commit](https://github.com/rails/rails/commit/3f2b7d60a52ffb2ad2d4fcf889c06b631db1946b)) + +* Removed deprecated methods related to controller filters. + ([Commit](https://github.com/rails/rails/commit/d7be30e8babf5e37a891522869e7b0191b79b757)) + +### Deprecations + +* Deprecated `config.action_controller.raise_on_unfiltered_parameters`. + It doesn't have any effect in Rails 5.1. + ([Commit](https://github.com/rails/rails/commit/c6640fb62b10db26004a998d2ece98baede509e5)) + +### Notable changes + +* Added the `direct` and `resolve` methods to the routing DSL. + ([Pull Request](https://github.com/rails/rails/pull/23138)) + +* Added a new `ActionDispatch::SystemTestCase` class to write system tests in + your applications. + ([Pull Request](https://github.com/rails/rails/pull/26703)) + +Action View +------------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +* Removed deprecated `#original_exception` in `ActionView::Template::Error`. + ([commit](https://github.com/rails/rails/commit/b9ba263e5aaa151808df058f5babfed016a1879f)) + +* Remove the option `encode_special_chars` misnomer from `strip_tags`. + ([Pull Request](https://github.com/rails/rails/pull/28061)) + +### Deprecations + +* Deprecated Erubis ERB handler in favor of Erubi. + ([Pull Request](https://github.com/rails/rails/pull/27757)) + +### Notable changes + +* Raw template handler (the default template handler in Rails 5) now outputs + HTML-safe strings. + ([commit](https://github.com/rails/rails/commit/1de0df86695f8fa2eeae6b8b46f9b53decfa6ec8)) + +* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` + fields. + ([Pull Request](https://github.com/rails/rails/pull/28061)) + +* New Builder-style syntax for HTML tags (`tag.div`, `tag.br`, etc.) + ([Pull Request](https://github.com/rails/rails/pull/25543)) + +* Add `form_with` to unify `form_tag` and `form_for` usage. + ([Pull Request](https://github.com/rails/rails/pull/26976)) + +* Add `check_parameters` option to `current_page?`. + ([Pull Request](https://github.com/rails/rails/pull/27549)) + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Notable changes + +* Allowed setting custom content type when attachments are included + and body is set inline. + ([Pull Request](https://github.com/rails/rails/pull/27227)) + +* Allowed passing lambdas as values to the `default` method. + ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf)) + +* Added support for parameterized invocation of mailers to share before filters and defaults + between different mailer actions. + ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf)) + +* Passed the incoming arguments to the mailer action to `process.action_mailer` event under + an `args` key. + ([Pull Request](https://github.com/rails/rails/pull/27900)) + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +### Removals + +* Removed support for passing arguments and block at the same time to + `ActiveRecord::QueryMethods#select`. + ([Commit](https://github.com/rails/rails/commit/4fc3366d9d99a0eb19e45ad2bf38534efbf8c8ce)) + +* Removed deprecated `activerecord.errors.messages.restrict_dependent_destroy.one` and + `activerecord.errors.messages.restrict_dependent_destroy.many` i18n scopes. + ([Commit](https://github.com/rails/rails/commit/00e3973a311)) + +* Removed deprecated force reload argument in singular and collection association readers. + ([Commit](https://github.com/rails/rails/commit/09cac8c67af)) + +* Removed deprecated support for passing a column to `#quote`. + ([Commit](https://github.com/rails/rails/commit/e646bad5b7c)) + +* Removed deprecated `name` arguments from `#tables`. + ([Commit](https://github.com/rails/rails/commit/d5be101dd02214468a27b6839ffe338cfe8ef5f3)) + +* Removed deprecated behavior of `#tables` and `#table_exists?` to return tables and views + to return only tables and not views. + ([Commit](https://github.com/rails/rails/commit/5973a984c369a63720c2ac18b71012b8347479a8)) + +* Removed deprecated `original_exception` argument in `ActiveRecord::StatementInvalid#initialize` + and `ActiveRecord::StatementInvalid#original_exception`. + ([Commit](https://github.com/rails/rails/commit/bc6c5df4699d3f6b4a61dd12328f9e0f1bd6cf46)) + +* Removed deprecated support of passing a class as a value in a query. + ([Commit](https://github.com/rails/rails/commit/b4664864c972463c7437ad983832d2582186e886)) + +* Removed deprecated support to query using commas on LIMIT. + ([Commit](https://github.com/rails/rails/commit/fc3e67964753fb5166ccbd2030d7382e1976f393)) + +* Removed deprecated `conditions` parameter from `#destroy_all`. + ([Commit](https://github.com/rails/rails/commit/d31a6d1384cd740c8518d0bf695b550d2a3a4e9b)) + +* Removed deprecated `conditions` parameter from `#delete_all`. + ([Commit](https://github.com/rails/rails/pull/27503/commits/e7381d289e4f8751dcec9553dcb4d32153bd922b)) + +* Removed deprecated method `#load_schema_for` in favor of `#load_schema`. + ([Commit](https://github.com/rails/rails/commit/419e06b56c3b0229f0c72d3e4cdf59d34d8e5545)) + +* Removed deprecated `#raise_in_transactional_callbacks` configuration. + ([Commit](https://github.com/rails/rails/commit/8029f779b8a1dd9848fee0b7967c2e0849bf6e07)) + +* Removed deprecated `#use_transactional_fixtures` configuration. + ([Commit](https://github.com/rails/rails/commit/3955218dc163f61c932ee80af525e7cd440514b3)) + +### Deprecations + +* Deprecated `error_on_ignored_order_or_limit` flag in favor of + `error_on_ignored_order`. + ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7)) + +* Deprecated `sanitize_conditions` in favor of `sanitize_sql`. + ([Pull Request](https://github.com/rails/rails/pull/25999)) + +* Deprecated `supports_migrations?` on connection adapters. + ([Pull Request](https://github.com/rails/rails/pull/28172)) + +* Deprecated `Migrator.schema_migrations_table_name`, use `SchemaMigration.table_name` instead. + ([Pull Request](https://github.com/rails/rails/pull/28351)) + +* Deprecated using `#quoted_id` in quoting and type casting. + ([Pull Request](https://github.com/rails/rails/pull/27962)) + +* Deprecated passing `default` argument to `#index_name_exists?`. + ([Pull Request](https://github.com/rails/rails/pull/26930)) + +### Notable changes + +* Change Default Primary Keys to BIGINT. + ([Pull Request](https://github.com/rails/rails/pull/26266)) + +* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+. + ([Commit](https://github.com/rails/rails/commit/65bf1c60053e727835e06392d27a2fb49665484c)) + +* Added support for limits in batch processing. + ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7)) + +* Transactional tests now wrap all Active Record connections in database + transactions. + ([Pull Request](https://github.com/rails/rails/pull/28726)) + +* Skipped comments in the output of `mysqldump` command by default. + ([Pull Request](https://github.com/rails/rails/pull/23301)) + +* Fixed `ActiveRecord::Relation#count` to use Ruby's `Enumerable#count` for counting + records when a block is passed as argument instead of silently ignoring the + passed block. + ([Pull Request](https://github.com/rails/rails/pull/24203)) + +* Pass `"-v ON_ERROR_STOP=1"` flag with `psql` command to not suppress SQL errors. + ([Pull Request](https://github.com/rails/rails/pull/24773)) + +* Add `ActiveRecord::Base.connection_pool.stat`. + ([Pull Request](https://github.com/rails/rails/pull/26988)) + +* Inheriting directly from `ActiveRecord::Migration` raises an error. + Specify the Rails version for which the migration was written for. + ([Commit](https://github.com/rails/rails/commit/249f71a22ab21c03915da5606a063d321f04d4d3)) + +* An error is raised when `through` association has ambiguous reflection name. + ([Commit](https://github.com/rails/rails/commit/0944182ad7ed70d99b078b22426cbf844edd3f61)) + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +* Removed deprecated methods in `ActiveModel::Errors`. + ([commit](https://github.com/rails/rails/commit/9de6457ab0767ebab7f2c8bc583420fda072e2bd)) + +* Removed deprecated `:tokenizer` option in the length validator. + ([commit](https://github.com/rails/rails/commit/6a78e0ecd6122a6b1be9a95e6c4e21e10e429513)) + +* Remove deprecated behavior that halts callbacks when the return value is false. + ([commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe)) + +### Notable changes + +* The original string assigned to a model attribute is no longer incorrectly + frozen. + ([Pull Request](https://github.com/rails/rails/pull/28729)) + +Active Job +----------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Removals + +* Removed deprecated support to passing the adapter class to `.queue_adapter`. + ([commit](https://github.com/rails/rails/commit/d1fc0a5eb286600abf8505516897b96c2f1ef3f6)) + +* Removed deprecated `#original_exception` in `ActiveJob::DeserializationError`. + ([commit](https://github.com/rails/rails/commit/d861a1fcf8401a173876489d8cee1ede1cecde3b)) + +### Notable changes + +* Added declarative exception handling via `ActiveJob::Base.retry_on` and `ActiveJob::Base.discard_on`. + ([Pull Request](https://github.com/rails/rails/pull/25991)) + +* Yield the job instance so you have access to things like `job.arguments` on + the custom logic after retries fail. + ([commit](https://github.com/rails/rails/commit/a1e4c197cb12fef66530a2edfaeda75566088d1f)) + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +* Removed the `ActiveSupport::Concurrency::Latch` class. + ([Commit](https://github.com/rails/rails/commit/0d7bd2031b4054fbdeab0a00dd58b1b08fb7fea6)) + +* Removed `halt_callback_chains_on_return_false`. + ([Commit](https://github.com/rails/rails/commit/4e63ce53fc25c3bc15c5ebf54bab54fa847ee02a)) + +* Removed deprecated behavior that halts callbacks when the return is false. + ([Commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe)) + +### Deprecations + +* The top level `HashWithIndifferentAccess` class has been softly deprecated + in favor of the `ActiveSupport::HashWithIndifferentAccess` one. + ([Pull Request](https://github.com/rails/rails/pull/28157)) + +* Deprecated passing string to `:if` and `:unless` conditional options on `set_callback` and `skip_callback`. + ([Commit](https://github.com/rails/rails/commit/0952552) + +### Notable changes + +* Fixed duration parsing and traveling to make it consistent across DST changes. + ([Commit](https://github.com/rails/rails/commit/8931916f4a1c1d8e70c06063ba63928c5c7eab1e), + [Pull Request](https://github.com/rails/rails/pull/26597)) + +* Updated Unicode to version 9.0.0. + ([Pull Request](https://github.com/rails/rails/pull/27822)) + +* Add Duration#before and #after as aliases for #ago and #since. + ([Pull Request](https://github.com/rails/rails/pull/27721)) + +* Added `Module#delegate_missing_to` to delegate method calls not + defined for the current object to a proxy object. + ([Pull Request](https://github.com/rails/rails/pull/23930)) + +* Added `Date#all_day` which returns a range representing the whole day + of the current date & time. + ([Pull Request](https://github.com/rails/rails/pull/24930)) + +* Introduced the `assert_changes` and `assert_no_changes` methods for tests. + ([Pull Request](https://github.com/rails/rails/pull/25393)) + +* The `travel` and `travel_to` methods now raise on nested calls. + ([Pull Request](https://github.com/rails/rails/pull/24890)) + +* Update `DateTime#change` to support usec and nsec. + ([Pull Request](https://github.com/rails/rails/pull/28242)) + +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-1-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index f50bcddbe7..8afec00018 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -1,8 +1,8 @@ -<h2>Ruby on Rails Guides (<%= @edge ? @version[0, 7] : @version %>)</h2> +<h2>Ruby on Rails Guides (<%= @edge ? @edge[0, 7] : @version %>)</h2> <% if @edge %> <p> - These are <b>Edge Guides</b>, based on the current <a href="https://github.com/rails/rails/tree/<%= @version %>">master</a> branch. + These are <b>Edge Guides</b>, based on <a href="https://github.com/rails/rails/tree/<%= @edge %>">master@<%= @edge[0, 7] %></a>. </p> <p> If you are looking for the ones for the stable version, please check @@ -10,12 +10,13 @@ </p> <% else %> <p> - These are the new guides for Rails 5.0 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. + These are the new guides for Rails 5.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. </p> <% end %> <p> The guides for earlier releases: +<a href="http://guides.rubyonrails.org/v5.0/">Rails 5.0</a>, <a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>, <a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>, <a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>, diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 4b9a22101a..50a28571b4 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -6,7 +6,7 @@ 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 on backend and frontend * How to setup Action Cable * How to setup channels * Deployment and Architecture setup for running Action Cable @@ -62,10 +62,10 @@ module ApplicationCable self.current_user = find_verified_user end - protected + private def find_verified_user - if current_user = User.find_by(id: cookies.signed[:user_id]) - current_user + if verified_user = User.find_by(id: cookies.signed[:user_id]) + verified_user else reject_unauthorized_connection end @@ -240,9 +240,9 @@ WebNotificationsChannel.broadcast_to( ``` The `WebNotificationsChannel.broadcast_to` call places a message in the current -subscription adapter (Redis by default)'s pubsub queue under a separate -broadcasting name for each user. For a user with an ID of 1, the broadcasting -name would be `web_notifications:1`. +subscription adapter (by default `redis` for production and `async` for development and +test environments)'s pubsub queue under a separate broadcasting name for each user. +For a user with an ID of 1, the broadcasting name would be `web_notifications:1`. The channel has been instructed to stream everything that arrives at `web_notifications:1` directly to the client by invoking the `received` @@ -422,7 +422,7 @@ App.cable.subscriptions.create "AppearanceChannel", buttonSelector = "[data-behavior~=appear_away]" install: -> - $(document).on "page:change.appearance", => + $(document).on "turbolinks:load.appearance", => @appear() $(document).on "click.appearance", buttonSelector, => @@ -530,7 +530,7 @@ Action Cable has two required configurations: a subscription adapter and allowed ### Subscription Adapter By default, Action Cable looks for a configuration file in `config/cable.yml`. -The file must specify an adapter and a URL for each Rails environment. See the +The file must specify an adapter for each Rails environment. See the [Dependencies](#dependencies) section for additional information on adapters. ```yaml @@ -543,7 +543,28 @@ test: production: adapter: redis url: redis://10.10.3.153:6381 + channel_prefix: appname_production ``` +#### Adapter Configuration + +Below is a list of the subscription adapters available for end users. + +##### Async Adapter + +The async adapter is intended for development/testing and should not be used in production. + +##### Redis Adapter + +Action Cable contains two Redis adapters: "normal" Redis and Evented Redis. Both +of the adapters require users to provide a URL pointing to the Redis server. +Additionally, a channel_prefix may be provided to avoid channel name collisions +when using the same Redis server for multiple applications. See the [Redis PubSub documentation](https://redis.io/topics/pubsub#database-amp-scoping) for more details. + +##### PostgreSQL Adapter + +The PostgreSQL adapter uses Active Record's connection pool, and thus the +application's `config/database.yml` database configuration, for its connection. +This may change in the future. [#27214](https://github.com/rails/rails/issues/27214) ### Allowed Request Origins diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 7b1138c7d4..22537f960c 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -61,7 +61,7 @@ end The [Layouts & Rendering Guide](layouts_and_rendering.html) explains this in more detail. -`ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the API documentation or in the source itself. +`ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the [API documentation](http://api.rubyonrails.org/classes/ActionController.html) or in the source itself. Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with `private` or `protected`) which are not intended to be actions, like auxiliary methods or filters. @@ -258,6 +258,17 @@ scalar values, map the key to an empty array: params.permit(id: []) ``` +Sometimes it is not possible or convenient to declare the valid keys of +a hash parameter or its internal structure. Just map to an empty hash: + +```ruby +params.permit(preferences: {}) +``` + +but be careful because this opens the door to arbitrary input. In this +case, `permit` ensures values in the returned structure are permitted +scalars and filters out anything else. + To whitelist an entire hash of parameters, the `permit!` method can be used: @@ -265,9 +276,10 @@ used: params.require(:log_entry).permit! ``` -This will mark the `:log_entry` parameters hash and any sub-hash of it as -permitted. Extreme care should be taken when using `permit!`, as it -will allow all current and future model attributes to be mass-assigned. +This marks the `:log_entry` parameters hash and any sub-hash of it as +permitted and does not check for permitted scalars, anything is accepted. +Extreme care should be taken when using `permit!`, as it will allow all current +and future model attributes to be mass-assigned. #### Nested Parameters @@ -703,11 +715,14 @@ end Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. +NOTE: Calling the same filter multiple times with different options will not work, +since the last filter definition will overwrite the previous ones. + ### After Filters and Around Filters In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. -"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. +"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. Please note that "after" filters are executed only after a successful action, but not when an exception is raised in the request cycle. "around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 34847832fd..7751ac00df 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -160,8 +160,8 @@ When you call the `mail` method now, Action Mailer will detect the two templates #### Calling the Mailer Mailers are really just another way to render a view. Instead of rendering a -view and sending out the HTTP protocol, they are just sending it out through the -email protocols instead. Due to this, it makes sense to just have your +view and sending it over the HTTP protocol, they are just sending it out through +the email protocols instead. Due to this, it makes sense to just have your controller tell the Mailer to send an email when a user is successfully created. Setting this up is painfully simple. @@ -400,7 +400,7 @@ class UserMailer < ApplicationMailer mail(to: @user.email, subject: 'Welcome to My Awesome Site') do |format| format.html { render 'another_template' } - format.text { render text: 'Render text' } + format.text { render plain: 'Render text' } end end end @@ -525,7 +525,7 @@ By using the full URL, your links will now work in your emails. #### Generating URLs with `url_for` -`url_for` generate full URL by default in templates. +`url_for` generates a full URL by default in templates. If you did not configure the `:host` option globally make sure to pass it to `url_for`. @@ -550,8 +550,9 @@ url helper. <%= user_url(@user, host: 'example.com') %> ``` -NOTE: non-`GET` links require [jQuery UJS](https://github.com/rails/jquery-ujs) -and won't work in mailer templates. They will result in normal `GET` requests. +NOTE: non-`GET` links require [rails-ujs](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts) or +[jQuery UJS](https://github.com/rails/jquery-ujs), and won't work in mailer templates. +They will result in normal `GET` requests. ### Adding images in Action Mailer Views @@ -574,7 +575,7 @@ Now you can display an image inside your email. ### Sending Multipart Emails Action Mailer will automatically send multipart emails if you have different -templates for the same action. So, for our UserMailer example, if you have +templates for the same action. So, for our `UserMailer` example, if you have `welcome_email.text.erb` and `welcome_email.html.erb` in `app/views/user_mailer`, Action Mailer will automatically send a multipart email with the HTML and text versions setup as different parts. @@ -780,7 +781,8 @@ config.action_mailer.smtp_settings = { 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 or +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, +then you will need to set an [app password](https://myaccount.google.com/apppasswords) and use that instead of your regular password. Alternatively, you can use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider. Mailer Testing diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index ff0127522b..10412128cc 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -419,7 +419,7 @@ image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png #### auto_discovery_link_tag -Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. +Returns a link tag that browsers and feed readers can use to auto-detect an RSS, Atom, or JSON feed. ```ruby auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => @@ -1493,7 +1493,7 @@ strip_links('Blog: <a href="http://myblog.com/">Visit</a>.') #### strip_tags(html) Strips all HTML tags from the html, including comments. -This uses the html-scanner tokenizer and so its HTML parsing ability is limited by that of html-scanner. +This functionality is powered by the rails-html-sanitizer gem. ```ruby strip_tags("Strip <i>these</i> tags!") diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index c65d1e6de5..443be77934 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -114,7 +114,7 @@ For enqueuing and executing jobs in production you need to set up a queuing back that is to say you need to decide for a 3rd-party queuing library that Rails should use. Rails itself only provides an in-process queuing system, which only keeps the jobs in RAM. If the process crashes or the machine is reset, then all outstanding jobs are lost with the -default async back-end. This may be fine for smaller apps or non-critical jobs, but most +default async backend. This may be fine for smaller apps or non-critical jobs, but most production apps will need to pick a persistent backend. ### Backends @@ -310,6 +310,12 @@ UserMailer.welcome(@user).deliver_now UserMailer.welcome(@user).deliver_later ``` +NOTE: Using the asynchronous queue from a Rake task (for example, to +send an email using `.deliver_later`) will generally not work because Rake will +likely end, causing the in-process thread pool to be deleted, before any/all +of the `.deliver_later` emails are processed. To avoid this problem, use +`.deliver_now` or run a persistent queue in development. + Internationalization -------------------- diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 732e553c62..b8f076a27b 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -87,7 +87,7 @@ end ### Conversion If a class defines `persisted?` and `id` methods, then you can include the -`ActiveModel::Conversion` module in that class and call the Rails conversion +`ActiveModel::Conversion` module in that class, and call the Rails conversion methods on objects of that class. ```ruby @@ -156,16 +156,17 @@ person.changed? # => false person.first_name = "First Name" person.first_name # => "First Name" -# returns true if any of the attributes have unsaved changes, false otherwise. +# returns true if any of the attributes have unsaved changes. person.changed? # => true # returns a list of attributes that have changed before saving. person.changed # => ["first_name"] -# returns a hash of the attributes that have changed with their original values. +# returns a Hash of the attributes that have changed with their original values. person.changed_attributes # => {"first_name"=>nil} -# returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field. +# returns a Hash of changes, with the attribute names as the keys, and the +# values as an array of the old and new values for that field. person.changes # => {"first_name"=>[nil, "First Name"]} ``` @@ -179,7 +180,7 @@ person.first_name # => "First Name" person.first_name_changed? # => true ``` -Track what was the previous value of the attribute. +Track the previous value of the attribute. ```ruby # attr_name_was accessor @@ -187,7 +188,7 @@ person.first_name_was # => nil ``` Track both previous and current value of the changed attribute. Returns an array -if changed, else returns nil. +if changed, otherwise returns nil. ```ruby # attr_name_change @@ -197,7 +198,7 @@ person.last_name_change # => nil ### Validations -The `ActiveModel::Validations` module adds the ability to validate class objects +The `ActiveModel::Validations` module adds the ability to validate objects like in Active Record. ```ruby @@ -225,7 +226,7 @@ person.valid? # => raises ActiveModel::StrictValidationFa ### Naming -`ActiveModel::Naming` adds a number of class methods which make the naming and routing +`ActiveModel::Naming` adds a number of class methods which make naming and routing easier to manage. The module defines the `model_name` class method which will define a number of accessors using some `ActiveSupport::Inflector` methods. @@ -248,7 +249,7 @@ Person.model_name.singular_route_key # => "person" ### Model -`ActiveModel::Model` adds the ability to a class to work with Action Pack and +`ActiveModel::Model` adds the ability for a class to work with Action Pack and Action View right out of the box. ```ruby @@ -293,7 +294,7 @@ objects. ### Serialization `ActiveModel::Serialization` provides basic serialization for your object. -You need to declare an attributes hash which contains the attributes you want to +You need to declare an attributes Hash which contains the attributes you want to serialize. Attributes must be strings, not symbols. ```ruby @@ -308,7 +309,7 @@ class Person end ``` -Now you can access a serialized hash of your object using the `serializable_hash`. +Now you can access a serialized Hash of your object using the `serializable_hash` method. ```ruby person = Person.new @@ -319,13 +320,14 @@ person.serializable_hash # => {"name"=>"Bob"} #### ActiveModel::Serializers -Rails provides an `ActiveModel::Serializers::JSON` serializer. -This module automatically include the `ActiveModel::Serialization`. +Active Model also provides the `ActiveModel::Serializers::JSON` module +for JSON serializing / deserializing. This module automatically includes the +previously discussed `ActiveModel::Serialization` module. ##### ActiveModel::Serializers::JSON -To use the `ActiveModel::Serializers::JSON` you only need to change from -`ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`. +To use `ActiveModel::Serializers::JSON` you only need to change the +module you are including from `ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`. ```ruby class Person @@ -339,7 +341,8 @@ class Person end ``` -With the `as_json` method you have a hash representing the model. +The `as_json` method, similar to `serializable_hash`, provides a Hash representing +the model. ```ruby person = Person.new @@ -348,8 +351,8 @@ person.name = "Bob" person.as_json # => {"name"=>"Bob"} ``` -From a JSON string you define the attributes of the model. -You need to have the `attributes=` method defined on your class: +You can also define the attributes for a model from a JSON string. +However, you need to define the `attributes=` method on your class: ```ruby class Person @@ -369,7 +372,7 @@ class Person end ``` -Now it is possible to create an instance of person and set the attributes using `from_json`. +Now it is possible to create an instance of `Person` and set attributes using `from_json`. ```ruby json = { name: 'Bob' }.to_json @@ -389,8 +392,8 @@ class Person end ``` -With the `human_attribute_name` you can transform attribute names into a more -human format. The human format is defined in your locale file. +With the `human_attribute_name` method, you can transform attribute names into a +more human-readable format. The human-readable format is defined in your locale file(s). * config/locales/app.pt-BR.yml @@ -411,7 +414,7 @@ Person.human_attribute_name('name') # => "Nome" `ActiveModel::Lint::Tests` allows you to test whether an object is compliant with the Active Model API. -* app/models/person.rb +* `app/models/person.rb` ```ruby class Person @@ -419,7 +422,7 @@ the Active Model API. end ``` -* test/models/person_test.rb +* `test/models/person_test.rb` ```ruby require 'test_helper' @@ -454,9 +457,9 @@ features out of the box. ### SecurePassword `ActiveModel::SecurePassword` provides a way to securely store any -password in an encrypted form. On including this module, a +password in an encrypted form. When you include this module, a `has_secure_password` class method is provided which defines -an accessor named `password` with certain validations on it. +a `password` accessor with certain validations on it. #### Requirements @@ -466,7 +469,7 @@ In order to make this work, the model must have an accessor named `password_dige The `has_secure_password` will add the following validations on the `password` accessor: 1. Password should be present. -2. Password should be equal to its confirmation (provided +password_confirmation+ is passed along). +2. Password should be equal to its confirmation (provided `password_confirmation` is passed along). 3. The maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends) #### Examples diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 2a1c960887..b1705855d0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -36,7 +36,7 @@ class User < ApplicationRecord before_validation :ensure_login_has_a_value - protected + private def ensure_login_has_a_value if login.nil? self.login = email unless email.blank? @@ -66,7 +66,7 @@ class User < ApplicationRecord # :on takes an array as well after_validation :set_location, on: [ :create, :update ] - protected + private def normalize_name self.name = name.downcase.titleize end @@ -77,7 +77,7 @@ class User < ApplicationRecord end ``` -It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. +It is considered good practice to declare callback methods as private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. Available Callbacks ------------------- @@ -117,6 +117,10 @@ Here is a list with all the available Active Record callbacks, listed in the sam WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed. +NOTE: `before_destroy` callbacks should be placed before `dependent: :destroy` +associations (or use the `prepend: true` option), to ensure they execute before +the records are deleted by `dependent: :destroy`. + ### `after_initialize` and `after_find` The `after_initialize` callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method. @@ -202,11 +206,9 @@ The following methods trigger callbacks: * `create` * `create!` -* `decrement!` * `destroy` * `destroy!` * `destroy_all` -* `increment!` * `save` * `save!` * `save(validate: false)` @@ -256,7 +258,11 @@ Halting Execution As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed. -The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception. +The whole callback chain is wrapped in a transaction. If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued. To intentionally stop a chain use: + +```ruby +throw :abort +``` WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. @@ -290,7 +296,7 @@ Article destroyed Conditional Callbacks --------------------- -As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. +As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. ### Using `:if` and `:unless` with a `Symbol` @@ -302,16 +308,6 @@ class Order < ApplicationRecord end ``` -### Using `:if` and `:unless` with a String - -You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition: - -```ruby -class Order < ApplicationRecord - before_save :normalize_card_number, if: "paid_with_card?" -end -``` - ### Using `:if` and `:unless` with a `Proc` Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index a45becf670..7fdb5901f3 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -229,7 +229,7 @@ As always, what has been generated for you is just a starting point. You can add or remove from it as you see fit by editing the `db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file. -Also, the generator accepts column type as `references`(also available as +Also, the generator accepts column type as `references` (also available as `belongs_to`). For instance: ```bash @@ -467,6 +467,8 @@ the first time (i.e. on the date the migration is applied). Some adapters may support additional options; see the adapter specific API docs for further information. +NOTE: `null` and `default` cannot be specified via command line. + ### Foreign Keys While it's not required you might want to add foreign key constraints to @@ -956,10 +958,10 @@ ActiveRecord::Schema.define(version: 20080906171750) do create_table "products", force: true do |t| t.string "name" - t.text "description" + t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.string "part_number" + t.string "part_number" end end ``` @@ -970,11 +972,11 @@ on. Because this is database-independent, it could be loaded into any database that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. -There is however a trade-off: `db/schema.rb` cannot express database specific -items such as triggers, stored procedures or check constraints. While in a -migration you can execute custom SQL statements, the schema dumper cannot -reconstitute those statements from the database. If you are using features like -this, then you should set the schema format to `:sql`. +NOTE: `db/schema.rb` cannot express database specific items such as triggers, +sequences, stored procedures or check constraints, etc. Please note that while +custom SQL statements can be run in migrations, these statements cannot be reconstituted +by the schema dumper. If you are using features like this, then you +should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` @@ -1018,10 +1020,10 @@ such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ -The main purpose of Rails' migration feature is to issue commands that modify the -schema using a consistent process. Migrations can also be used -to add or modify data. This is useful in an existing database that can't be destroyed -and recreated, such as a production database. +The main purpose of Rails' migration feature is to issue commands that modify the +schema using a consistent process. Migrations can also be used +to add or modify data. This is useful in an existing database that can't be destroyed +and recreated, such as a production database. ```ruby class AddInitialProducts < ActiveRecord::Migration[5.0] @@ -1037,10 +1039,10 @@ class AddInitialProducts < ActiveRecord::Migration[5.0] end ``` -To add initial data after a database is created, Rails has a built-in -'seeds' feature that makes the process quick and easy. This is especially -useful when reloading the database frequently in development and test environments. -It's easy to get started with this feature: just fill up `db/seeds.rb` with some +To add initial data after a database is created, Rails has a built-in +'seeds' feature that makes the process quick and easy. This is especially +useful when reloading the database frequently in development and test environments. +It's easy to get started with this feature: just fill up `db/seeds.rb` with some Ruby code, and run `rails db:seed`: ```ruby diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index d7e35490ef..6d07291b07 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -111,7 +111,7 @@ profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} profile.save! Profile.where("settings->'color' = ?", "yellow") -#=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]> +# => #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]> ``` ### JSON @@ -422,7 +422,7 @@ device = Device.create device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e" ``` -NOTE: `uuid_generate_v4()` (from `uuid-ossp`) is assumed if no `:default` option was +NOTE: `gen_random_uuid()` (from `pgcrypto`) is assumed if no `:default` option was passed to `create_table`. Full Text Search diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index bbe1b0decc..3676462788 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -81,7 +81,6 @@ The methods are: * `reorder` * `reverse_order` * `select` -* `distinct` * `where` Finder methods that return a collection, such as `where` and `group`, return an instance of `ActiveRecord::Relation`. Methods that find a single entity, such as `find` and `first`, return a single instance of the model. @@ -119,7 +118,7 @@ You can also use this method to query for multiple objects. Call the `find` meth ```ruby # Find the clients with primary keys 1 and 10. -client = Client.find([1, 10]) # Or even Client.find(1, 10) +clients = Client.find([1, 10]) # Or even Client.find(1, 10) # => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] ``` @@ -151,7 +150,7 @@ The `take` method returns `nil` if no record is found and no exception will be r You can pass in a numerical argument to the `take` method to return up to that number of results. For example ```ruby -client = Client.take(2) +clients = Client.take(2) # => [ # #<Client id: 1, first_name: "Lifo">, # #<Client id: 220, first_name: "Sara"> @@ -190,7 +189,7 @@ If your [default scope](active_record_querying.html#applying-a-default-scope) co You can pass in a numerical argument to the `first` method to return up to that number of results. For example ```ruby -client = Client.first(3) +clients = Client.first(3) # => [ # #<Client id: 1, first_name: "Lifo">, # #<Client id: 2, first_name: "Fifo">, @@ -241,7 +240,7 @@ If your [default scope](active_record_querying.html#applying-a-default-scope) co You can pass in a numerical argument to the `last` method to return up to that number of results. For example ```ruby -client = Client.last(3) +clients = Client.last(3) # => [ # #<Client id: 219, first_name: "James">, # #<Client id: 220, first_name: "Sara">, @@ -558,6 +557,19 @@ In other words, this query can be generated by calling `where` with no argument, SELECT * FROM clients WHERE (clients.locked != 1) ``` +### OR Conditions + +`OR` conditions between two relations can be built by calling `or` on the first +relation, and passing the second one as an argument. + +```ruby +Client.where(locked: true).or(Client.where(orders_count: [1,3,5])) +``` + +```sql +SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5)) +``` + Ordering -------- @@ -1382,8 +1394,9 @@ class Client < ApplicationRecord end ``` -NOTE: The `default_scope` is also applied while creating/building a record. -It is not applied while updating a record. E.g.: +NOTE: The `default_scope` is also applied while creating/building a record +when the scope arguments are given as a `Hash`. It is not applied while +updating a record. E.g.: ```ruby class Client < ApplicationRecord @@ -1394,6 +1407,17 @@ Client.new # => #<Client id: nil, active: true> Client.unscoped.new # => #<Client id: nil, active: nil> ``` +Be aware that, when given in the `Array` format, `default_scope` query arguments +cannot be converted to a `Hash` for default attribute assignment. E.g.: + +```ruby +class Client < ApplicationRecord + default_scope { where("active = ?", true) } +end + +Client.new # => #<Client id: nil, active: nil> +``` + ### Merging of scopes Just like `where` clauses scopes are merged using `AND` conditions. @@ -1545,7 +1569,7 @@ SELECT people.id, people.name, comments.text FROM people INNER JOIN comments ON comments.person_id = people.id -WHERE comments.created_at = '2015-01-01' +WHERE comments.created_at > '2015-01-01' ``` ### Retrieving specific data from multiple tables @@ -1869,7 +1893,7 @@ Which will execute: ```sql SELECT count(DISTINCT clients.id) AS count_all FROM clients - LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE + LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE (clients.first_name = 'Ryan' AND orders.status = 'received') ``` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 665e97c470..5313361dfd 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -490,9 +490,6 @@ If you set `:only_integer` to `true`, then it will use the regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using `Float`. -WARNING. Note that the regular expression above allows a trailing newline -character. - ```ruby class Player < ApplicationRecord validates :points, numericality: true @@ -916,18 +913,6 @@ class Order < ApplicationRecord end ``` -### Using a String with `:if` and `:unless` - -You can also use a string that will be evaluated using `eval` and needs to -contain valid Ruby code. You should use this option only when the string -represents a really short condition. - -```ruby -class Person < ApplicationRecord - validates :surname, presence: true, if: "name.nil?" -end -``` - ### Using a Proc with `:if` and `:unless` Finally, it's possible to associate `:if` and `:unless` with a `Proc` object diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 60a6c37f82..67bed4c8da 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -135,36 +135,53 @@ NOTE: Defined in `active_support/core_ext/object/blank.rb`. ### `duplicable?` -A few fundamental objects in Ruby are singletons. For example, in the whole life of a program the integer 1 refers always to the same instance: +In Ruby 2.4 most objects can be duplicated via `dup` or `clone` except +methods and certain numbers. Though Ruby 2.2 and 2.3 can't duplicate `nil`, +`false`, `true`, and symbols as well as instances `Float`, `Fixnum`, +and `Bignum` instances. ```ruby -1.object_id # => 3 -Math.cos(0).to_i.object_id # => 3 +"foo".dup # => "foo" +"".dup # => "" +1.method(:+).dup # => TypeError: allocator undefined for Method +Complex(0).dup # => TypeError: can't copy Complex ``` -Hence, there's no way these objects can be duplicated through `dup` or `clone`: +Active Support provides `duplicable?` to query an object about this: ```ruby -true.dup # => TypeError: can't dup TrueClass +"foo".duplicable? # => true +"".duplicable? # => true +Rational(1).duplicable? # => false +Complex(1).duplicable? # => false +1.method(:+).duplicable? # => false ``` -Some numbers which are not singletons are not duplicable either: +`duplicable?` matches Ruby's `dup` according to the Ruby version. + +So in 2.4: ```ruby -0.0.clone # => allocator undefined for Float -(2**1024).clone # => allocator undefined for Bignum +nil.dup # => nil +:my_symbol.dup # => :my_symbol +1.dup # => 1 + +nil.duplicable? # => true +:my_symbol.duplicable? # => true +1.duplicable? # => true ``` -Active Support provides `duplicable?` to programmatically query an object about this property: +Whereas in 2.2 and 2.3: ```ruby -"foo".duplicable? # => true -"".duplicable? # => true -0.0.duplicable? # => false -false.duplicable? # => false -``` +nil.dup # => TypeError: can't dup NilClass +:my_symbol.dup # => TypeError: can't dup Symbol +1.dup # => TypeError: can't dup Fixnum -By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, module, and method objects. +nil.duplicable? # => false +:my_symbol.duplicable? # => false +1.duplicable? # => false +``` WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case. @@ -511,56 +528,6 @@ NOTE: Defined in `active_support/core_ext/object/inclusion.rb`. Extensions to `Module` ---------------------- -### `alias_method_chain` - -**This method is deprecated in favour of using Module#prepend.** - -Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_. - -For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionDispatch::IntegrationTest#process` this way in `test/test_helper.rb`: - -```ruby -ActionDispatch::IntegrationTest.class_eval do - # save a reference to the original process method - alias_method :original_process, :process - - # now redefine process and delegate to original_process - def process('GET', path, params: nil, headers: nil, env: nil, xhr: false) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - original_process('GET', path, params: params) - end -end -``` - -That's the method `get`, `post`, etc., delegate the work to. - -That technique has a risk, it could be the case that `:original_process` was taken. To try to avoid collisions people choose some label that characterizes what the chaining is about: - -```ruby -ActionDispatch::IntegrationTest.class_eval do - def process_with_stringified_params(...) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(method, path, params: params) - end - alias_method :process_without_stringified_params, :process - alias_method :process, :process_with_stringified_params -end -``` - -The method `alias_method_chain` provides a shortcut for that pattern: - -```ruby -ActionDispatch::IntegrationTest.class_eval do - def process_with_stringified_params(...) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(method, path, params: params) - end - alias_method_chain :process, :stringified_params -end -``` - -NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. - ### Attributes #### `alias_attribute` @@ -2661,7 +2628,7 @@ The method `transform_keys` accepts a block and returns a hash that has applied ```ruby {nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase } -# => {"" => nil, "A" => :a, "1" => 1} +# => {"" => nil, "1" => 1, "A" => :a} ``` In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: @@ -2703,7 +2670,7 @@ The method `stringify_keys` returns a hash that has a stringified version of the ```ruby {nil => nil, 1 => 1, a: :a}.stringify_keys -# => {"" => nil, "a" => :a, "1" => 1} +# => {"" => nil, "1" => 1, "a" => :a} ``` In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: @@ -2745,7 +2712,7 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the ```ruby {nil => nil, 1 => 1, "a" => "a"}.symbolize_keys -# => {1=>1, nil=>nil, :a=>"a"} +# => {nil=>nil, 1=>1, :a=>"a"} ``` WARNING. Note in the previous example only one key was symbolized. @@ -2822,7 +2789,7 @@ Ruby has built-in support for taking slices out of strings and arrays. Active Su ```ruby {a: 1, b: 2, c: 3}.slice(:a, :c) -# => {:c=>3, :a=>1} +# => {:a=>1, :c=>3} {a: 1, b: 2, c: 3}.slice(:b, :X) # => {:b=>2} # non-existing keys are ignored diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 03af3cf819..03c9183eb3 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -226,17 +226,36 @@ Action View } ``` +### render_collection.action_view + +| Key | Value | +| ------------- | ------------------------------------- | +| `:identifier` | Full path to template | +| `:count` | Size of collection | +| `:cache_hits` | Number of partials fetched from cache | + +`:cache_hits` is only included if the collection is rendered with `cached: true`. + +```ruby +{ + identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb", + count: 3, + cache_hits: 0 +} +``` + Active Record ------------ ### sql.active_record -| Key | Value | -| ---------------- | --------------------- | -| `:sql` | SQL statement | -| `:name` | Name of the operation | -| `:connection_id` | `self.object_id` | -| `:binds` | Bind parameters | +| Key | Value | +| ---------------- | ---------------------------------------- | +| `:sql` | SQL statement | +| `:name` | Name of the operation | +| `:connection_id` | `self.object_id` | +| `:binds` | Bind parameters | +| `:cached` | `true` is added when cached queries used | INFO. The adapters will add their own data as well. diff --git a/guides/source/api_app.md b/guides/source/api_app.md index f373d313cc..64200ec242 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -206,16 +206,17 @@ An API application comes with the following middleware by default: - `ActiveSupport::Cache::Strategy::LocalCache::Middleware` - `Rack::Runtime` - `ActionDispatch::RequestId` +- `ActionDispatch::RemoteIp` - `Rails::Rack::Logger` - `ActionDispatch::ShowExceptions` - `ActionDispatch::DebugExceptions` -- `ActionDispatch::RemoteIp` - `ActionDispatch::Reloader` - `ActionDispatch::Callbacks` - `ActiveRecord::Migration::CheckPending` - `Rack::Head` - `Rack::ConditionalGet` - `Rack::ETag` +- `MyApi::Application::Routes` See the [internal middleware](rails_on_rack.html#internal-middleware-stack) section of the Rack guide for further information on them. @@ -360,7 +361,7 @@ middleware set, you can remove it with: config.middleware.delete ::Rack::Sendfile ``` -Keep in mind that removing these middleware will remove support for certain +Keep in mind that removing these middlewares will remove support for certain features in Action Controller. Choosing Controller Modules @@ -385,8 +386,9 @@ controller modules by default: hooks defined by Action Controller (see [the instrumentation guide](active_support_instrumentation.html#action-controller) for more information regarding this). -- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, +- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, so that you don't have to specify root elements sending POST requests for instance. +- `ActionController::Head`: Support for returning a response with no content, only headers Other plugins may add additional modules. You can get a list of all modules included into `ActionController::API` in the rails console: @@ -394,12 +396,12 @@ included into `ActionController::API` in the rails console: ```bash $ bin/rails c >> ActionController::API.ancestors - ActionController::Metal.ancestors -=> [ActionController::API, - ActiveRecord::Railties::ControllerRuntime, - ActionDispatch::Routing::RouteSet::MountedHelpers, - ActionController::ParamsWrapper, - ... , - AbstractController::Rendering, +=> [ActionController::API, + ActiveRecord::Railties::ControllerRuntime, + ActionDispatch::Routing::RouteSet::MountedHelpers, + ActionController::ParamsWrapper, + ... , + AbstractController::Rendering, ActionView::ViewPaths] ``` diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 34b9c0d2ca..c3c7367304 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -281,7 +281,7 @@ Methods created with `(module|class)_eval(STRING)` have a comment by their side ```ruby for severity in Severity.constants - class_eval <<-EOT, __FILE__, __LINE__ + class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) end # end @@ -333,10 +333,6 @@ As a contributor, it's important to think about whether this API is meant for en A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly. -If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project. - -A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails. - To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first. Regarding the Rails Stack diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 41dfeea84d..22b6b278d7 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -78,9 +78,9 @@ requests can mean faster loading for your application. Sprockets concatenates all JavaScript files into one master `.js` file and all CSS files into one master `.css` file. As you'll learn later in this guide, you can customize this strategy to group files any way you like. In production, -Rails inserts an MD5 fingerprint into each filename so that the file is cached -by the web browser. You can invalidate the cache by altering this fingerprint, -which happens automatically whenever you change the file contents. +Rails inserts an SHA256 fingerprint into each filename so that the file is +cached by the web browser. You can invalidate the cache by altering this +fingerprint, which happens automatically whenever you change the file contents. The second feature of the asset pipeline is asset minification or compression. For CSS files, this is done by removing whitespace and comments. For JavaScript, @@ -106,7 +106,7 @@ or in web browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change. This will cause the remote clients to request a new copy of the content. This is generally known as _cache busting_. -The technique sprockets uses for fingerprinting is to insert a hash of the +The technique Sprockets uses for fingerprinting is to insert a hash of the content into the name, usually at the end. For example a CSS file `global.css` ``` @@ -207,7 +207,7 @@ default .coffee and .scss files will not be precompiled on their own. See precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. -If you are using Mac OS X or Windows, you have a JavaScript runtime installed in +If you are using macOS or Windows, you have a JavaScript runtime installed in your operating system. Check [ExecJS](https://github.com/rails/execjs#readme) documentation to know all supported JavaScript runtimes. You can also disable generation of controller specific asset files by adding the @@ -335,7 +335,7 @@ an asset has been updated and if so loads it into the page: <%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %> ``` -In regular views you can access images in the `public/assets/images` directory +In regular views you can access images in the `app/assets/images` directory like this: ```erb @@ -346,9 +346,9 @@ Provided that the pipeline is enabled within your application (and not disabled in the current environment context), this file is served by Sprockets. If a file exists at `public/assets/rails.png` it is served by the web server. -Alternatively, a request for a file with an MD5 hash such as -`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same -way. How these hashes are generated is covered in the [In +Alternatively, a request for a file with an SHA256 hash such as +`public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png` +is treated the same way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide. Sprockets will also look through the paths specified in `config.assets.paths`, @@ -654,7 +654,7 @@ In the production environment Sprockets uses the fingerprinting scheme outlined above. By default Rails assumes assets have been precompiled and will be served as static assets by your web server. -During the precompilation phase an MD5 is generated from the contents of the +During the precompilation phase an SHA256 is generated from the contents of the compiled files, and inserted into the filenames as they are written to disk. These fingerprinted names are used by the Rails helpers in place of the manifest name. @@ -743,22 +743,24 @@ Rails.application.config.assets.precompile += %w( admin.js admin.css ) NOTE. Always specify an expected compiled filename that ends with .js or .css, even if you want to add Sass or CoffeeScript files to the precompile array. -The task also generates a `manifest-md5hash.json` that contains a list with -all your assets and their respective fingerprints. This is used by the Rails -helper methods to avoid handing the mapping requests back to Sprockets. A -typical manifest file looks like: +The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is +an MD5 hash) that contains a list with all your assets and their respective +fingerprints. This is used by the Rails helper methods to avoid handing the +mapping requests back to Sprockets. A typical manifest file looks like: ```ruby -{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506, -"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560, -"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591, -"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406, -"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646, -"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js": -"application-723d1be6cc741a3aabb1cec24276d681.js","application.css": -"application-1c5752789588ac18d7e1a50b1f0fd4c2.css", -"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png": -"my_image-231a680f23887d9dd70710ea5efd3c62.png"}} +{"files":{"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383, +"digest":"aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b","integrity":"sha256-ruS+cfEogDeueLmX3ziDMu39JGRxtTPc7aqPn+FWRCs="}, +"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css":{"logical_path":"application.css","mtime":"2016-12-23T19:12:20-05:00","size":2994, +"digest":"86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18","integrity":"sha256-hqKStQcHk8N+LA5fOfc7s4dkTq6tp/lub8BAoCixbBg="}, +"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico":{"logical_path":"favicon.ico","mtime":"2016-12-23T20:11:00-05:00","size":8629, +"digest":"8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda","integrity":"sha256-jSOHuNTTLOzZP6OQDfDp/4nQGqzYT1DngMF8n2s9Dto="}, +"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png":{"logical_path":"my_image.png","mtime":"2016-12-23T20:10:54-05:00","size":23414, +"digest":"f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493","integrity":"sha256-9AKBVv1+ygNYTV8vwEcN8eDbxzaequY4sv8DP5iOxJM="}}, +"assets":{"application.js":"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js", +"application.css":"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css", +"favicon.ico":"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico", +"my_image.png":"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png"}} ``` The default location for the manifest is the root of the location specified in @@ -850,7 +852,7 @@ config.assets.compile = true On the first request the assets are compiled and cached as outlined in development above, and the manifest names used in the helpers are altered to -include the MD5 hash. +include the SHA256 hash. Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This signals all caches between your server and the client browser that this content @@ -866,7 +868,7 @@ pre-existing JavaScript runtimes, you may want to add one to your Gemfile: ```ruby group :production do - gem 'therubyracer' + gem 'mini_racer' end ``` @@ -1115,7 +1117,7 @@ config.assets.js_compressor = :uglifier ``` NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme) -supported runtime in order to use `uglifier`. If you are using Mac OS X or +supported runtime in order to use `uglifier`. If you are using macOS or Windows you have a JavaScript runtime installed in your operating system. @@ -1227,35 +1229,25 @@ Sprockets. Making Your Library or Gem a Pre-Processor ------------------------------------------ -As Sprockets uses [Tilt](https://github.com/rtomayko/tilt) as a generic -interface to different templating engines, your gem should just implement the -Tilt template protocol. Normally, you would subclass `Tilt::Template` and -reimplement the `prepare` method, which initializes your template, and the -`evaluate` method, which returns the processed source. The original source is -stored in `data`. Have a look at -[`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb) -sources to learn more. +Sprockets uses Processors, Transformers, Compressors, and Exporters to extend +Sprockets functionality. Have a look at +[Extending Sprockets](https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md) +to learn more. Here we registered a preprocessor to add a comment to the end +of text/css (.css) files. ```ruby -module BangBang - class Template < ::Tilt::Template - def prepare - # Do any initialization here - end - - # Adds a "!" to original template. - def evaluate(scope, locals, &block) - "#{data}!" - end +module AddComment + def self.call(input) + { data: input[:data] + "/* Hello From my sprockets extension */" } end end ``` -Now that you have a `Template` class, it's time to associate it with an -extension for template files: +Now that you have a module that modifies the input data, it's time to register +it as a preprocessor for your mime type. ```ruby -Sprockets.register_engine '.bang', BangBang::Template +Sprockets.register_preprocessor 'text/css', AddComment ``` Upgrading from Old Versions of Rails diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 3837cda553..5c7d1f5365 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -154,7 +154,7 @@ case, the column definition might look like this: ```ruby create_table :accounts do |t| - t.belongs_to :supplier, index: true, unique: true, foreign_key: true + t.belongs_to :supplier, index: { unique: true }, foreign_key: true # ... end ``` @@ -582,14 +582,30 @@ class CreateBooks < ActiveRecord::Migration[5.0] t.string :book_number t.integer :author_id end - - add_index :books, :author_id 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: + +```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 + end + + add_index :books, :author_id + add_foreign_key :books, :authors + end +end +``` + #### 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. @@ -709,55 +725,73 @@ class Book < ApplicationRecord end ``` -By default, Active Record doesn't know about the connection between these associations. This can lead to two copies of an object getting out of sync: +Active Record will attempt to automatically identify that these two models share a bi-directional association based on the association name. In this way, Active Record will only load one copy of the `Author` object, making your application more efficient and preventing inconsistent data: ```ruby a = Author.first b = a.books.first a.first_name == b.author.first_name # => true -a.first_name = 'Manny' -a.first_name == b.author.first_name # => false +a.first_name = 'David' +a.first_name == b.author.first_name # => true ``` -This happens because `a` and `b.author` are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations: +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: + +* `:conditions` +* `:through` +* `:polymorphic` +* `:class_name` +* `:foreign_key` + +For example, consider the following model declarations: ```ruby class Author < ApplicationRecord - has_many :books, inverse_of: :author + has_many :books end class Book < ApplicationRecord - belongs_to :author, inverse_of: :books + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end ``` -With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient: +Active Record will no longer automatically recognize the bi-directional association: ```ruby a = Author.first b = a.books.first -a.first_name == b.author.first_name # => true -a.first_name = 'Manny' -a.first_name == b.author.first_name # => true +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => false +``` + +Active Record provides the `:inverse_of` option so you can explicitly declare bi-directional associations: + +```ruby +class Author < ApplicationRecord + has_many :books, inverse_of: 'writer' +end + +class Book < ApplicationRecord + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' +end ``` -There are a few limitations to `inverse_of` support: +By including the `:inverse_of` option in the `has_many` association declaration, Active Record will now recognize the bi-directional association: + +```ruby +a = Author.first +b = a.books.first +a.first_name == b.writer.first_name # => true +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. -* For `belongs_to` associations, `has_many` inverse associations are ignored. - -Every association will attempt to automatically find the inverse association -and set the `:inverse_of` option heuristically (based on the association name). -Most associations with standard names will be supported. However, associations -that contain the following options will not have their inverses set -automatically: - -* `:conditions` -* `:through` -* `:polymorphic` -* `:foreign_key` Detailed Association Reference ------------------------------ @@ -1383,7 +1417,7 @@ If either of these saves fails due to validation errors, then the assignment sta If the parent object (the one declaring the `has_one` association) is unsaved (that is, `new_record?` returns `true`) then the child objects are not saved. They will automatically when the parent object is saved. -If you want to assign an object to a `has_one` association without saving the object, use the `association.build` method. +If you want to assign an object to a `has_one` association without saving the object, use the `build_association` method. ### `has_many` Association Reference @@ -1525,7 +1559,7 @@ The `collection.size` method returns the number of objects in the collection. The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. ```ruby -@available_books = @author.books.find(1) +@available_book = @author.books.find(1) ``` ##### `collection.where(...)` @@ -1994,11 +2028,9 @@ The `collection.delete` method removes one or more objects from the collection b @part.assemblies.delete(@assembly1) ``` -WARNING: This does not trigger callbacks on the join records. - ##### `collection.destroy(object, ...)` -The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each record in the join table, including running callbacks. This does not destroy the objects. +The `collection.destroy` method removes one or more objects from the collection by deleting records in the join table. This does not destroy the objects. ```ruby @part.assemblies.destroy(@assembly1) diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 61657023e7..05743ee4ce 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -983,20 +983,19 @@ WHERE "polygons"."type" IN ("Rectangle") That is not a bug, the query includes all *known* descendants of `Rectangle`. A way to ensure this works correctly regardless of the order of execution is to -load the leaves of the tree by hand at the bottom of the file that defines the -root class: +manually load the direct subclasses at the bottom of the file that defines each +intermediate class: ```ruby -# app/models/polygon.rb -class Polygon < ApplicationRecord +# app/models/rectangle.rb +class Rectangle < Polygon end -require_dependency ‘square’ +require_dependency 'square' ``` -Only the leaves that are **at least grandchildren** need to be loaded this -way. Direct subclasses do not need to be preloaded. If the hierarchy is -deeper, intermediate classes will be autoloaded recursively from the bottom -because their constant will appear in the class definitions as superclass. +This needs to happen for every intermediate (non-root and non-leaf) class. The +root class does not scope the query by type, and therefore does not necessarily +have to know all its descendants. ### Autoloading and `require` diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index fd7626250c..6cdce5c2f4 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -387,6 +387,11 @@ store is not appropriate for large application deployments. However, it can work well for small, low traffic sites with only a couple of server processes, as well as development and test environments. +New Rails projects are configured to use this implementation in development environment by default. + +NOTE: Since processes will not share cache data when using `:memory_store`, +it will not be possible to manually read, write or expire the cache via the Rails console. + ### ActiveSupport::Cache::FileStore This cache store uses the file system to store entries. The path to the directory where the store files will be stored must be specified when initializing the cache. @@ -396,14 +401,15 @@ config.cache_store = :file_store, "/path/to/cache/directory" ``` With this cache store, multiple server processes on the same host can share a -cache. The cache store is appropriate for low to medium traffic sites that are +cache. This cache store is appropriate for low to medium traffic sites that are served off one or two hosts. Server processes running on different hosts could share a cache by using a shared file system, but that setup is not recommended. As the cache will grow until the disk is full, it is recommended to periodically clear out old entries. -This is the default cache store implementation. +This is the default cache store implementation (at `"#{root}/tmp/cache/"`) if +no explicit `config.cache_store` is supplied. ### ActiveSupport::Cache::MemCacheStore @@ -570,6 +576,20 @@ You can also set the strong ETag directly on the response. response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7" ``` +Caching in Development +---------------------- + +It's common to want to test the caching strategy of your application +in development mode. Rails provides the rake task `dev:cache` to +easily toggle caching on/off. + +```bash +$ bin/rails dev:cache +Development mode is now being cached. +$ bin/rails dev:cache +Development mode is no longer being cached. +``` + References ---------- diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 9d7ecce947..3360496c08 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -63,7 +63,7 @@ With no further work, `rails server` will run our new shiny Rails app: $ cd commandsapp $ bin/rails server => Booting Puma -=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000 +=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas @@ -294,7 +294,7 @@ If you wish to test out some code without changing any data, you can do that by ```bash $ bin/rails console --sandbox -Loading development environment in sandbox (Rails 5.0.0) +Loading development environment in sandbox (Rails 5.1.0) Any modifications you make will be rolled back on exit irb(main):001:0> ``` @@ -407,8 +407,8 @@ db:fixtures:load Loads fixtures into the ... db:migrate Migrate the database ... db:migrate:status Display status of migrations db:rollback Rolls the schema back to ... -db:schema:cache:clear Clears a db/schema_cache.dump file -db:schema:cache:dump Creates a db/schema_cache.dump file +db:schema:cache:clear Clears a db/schema_cache.yml file +db:schema:cache:dump Creates a db/schema_cache.yml file db:schema:dump Creates a db/schema.rb file ... db:schema:load Loads a schema.rb file ... db:seed Loads the seed data ... @@ -428,12 +428,12 @@ INFO: You can also use `bin/rails -T` to get the list of tasks. ```bash $ bin/rails about About your application's environment -Rails version 5.0.0 +Rails version 5.1.0 Ruby version 2.2.2 (x86_64-linux) RubyGems version 2.4.6 -Rack version 1.6 +Rack version 2.0.1 JavaScript Runtime Node.js (V8) -Middleware Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag +Middleware: Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, WebConsole::Middleware, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 diff --git a/guides/source/configuring.md b/guides/source/configuring.md index fbf3c27957..1234e1f192 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -32,7 +32,7 @@ Configuring Rails Components In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The configuration file `config/application.rb` and environment-specific configuration files (such as `config/environments/production.rb`) allow you to specify the various settings that you want to pass down to all of the components. -For example, the `config/application.rb` file includes this setting: +For example, you could add this setting to `config/application.rb` file: ```ruby config.time_zone = 'Central Time (US & Canada)' @@ -108,9 +108,9 @@ application. Accepts a valid week day symbol (e.g. `:monday`). you don't want shown in the logs, such as passwords or credit card numbers. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. -* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. +* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://api.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. -* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you. +* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you. * `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all environments. The available log levels are: `:debug`, @@ -175,10 +175,12 @@ pipeline is enabled. It is set to `true` by default. * `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder. -* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default. +* `config.assets.digest` enables the use of SHA256 fingerprints in asset names. Set to `true` by default. * `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`. +* `config.assets.version` is an option string that is used in SHA256 hash generation. This can be changed to force all files to be recompiled. + * `config.assets.compile` is a boolean that can be used to turn on live Sprockets compilation in production. * `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to `false` will turn off served assets logging. @@ -348,9 +350,9 @@ All these configuration options are delegated to the `I18n` library. `config/environments/production.rb` which is generated by Rails. The default value is `true` if this configuration is not set. -* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling db:structure:dump. - The options are `:schema_search_path` (the default) which dumps any schemas listed in schema_search_path, - `:all` which always dumps all schemas regardless of the schema_search_path, +* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling `db:structure:dump`. + The options are `:schema_search_path` (the default) which dumps any schemas listed in `schema_search_path`, + `:all` which always dumps all schemas regardless of the `schema_search_path`, or a string of comma separated schemas. * `config.active_record.belongs_to_required_by_default` is a boolean value and @@ -360,12 +362,17 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.warn_on_records_fetched_greater_than` allows setting a warning threshold for query result size. If the number of records returned by a query exceeds the threshold, a warning is logged. This can be used to - identify queries which might be causing memory bloat. + identify queries which might be causing a memory bloat. * `config.active_record.index_nested_attribute_errors` allows errors for nested - has_many relationships to be displayed with an index as well as the error. + `has_many` relationships to be displayed with an index as well as the error. Defaults to `false`. +* `config.active_record.use_schema_cache_dump` enables users to get schema cache information + from `db/schema_cache.yml` (generated by `bin/rails db:schema:cache:dump`), instead of + having to send a query to the database to get this information. + Defaults to `true`. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`. @@ -449,10 +456,14 @@ to `'http authentication'`. Defaults to `'signed cookie'`. * `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt -value. Defaults to `'encrypted cookie'`. + value. Defaults to `'encrypted cookie'`. * `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed -encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. + encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. + +* `config.action_dispatch.authenticated_encrypted_cookie_salt` sets the + authenticated encrypted cookie salt. Defaults to `'authenticated encrypted + cookie'`. * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) @@ -462,23 +473,23 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. ```ruby config.action_dispatch.rescue_responses = { - 'ActionController::RoutingError' => :not_found, - 'AbstractController::ActionNotFound' => :not_found, - 'ActionController::MethodNotAllowed' => :method_not_allowed, - 'ActionController::UnknownHttpMethod' => :method_not_allowed, - 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::UnknownFormat' => :not_acceptable, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, - 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, - 'ActionDispatch::ParamsParser::ParseError' => :bad_request, - 'ActionController::BadRequest' => :bad_request, - 'ActionController::ParameterMissing' => :bad_request, - 'Rack::QueryParser::ParameterTypeError' => :bad_request, - 'Rack::QueryParser::InvalidParameterError' => :bad_request, - 'ActiveRecord::RecordNotFound' => :not_found, - 'ActiveRecord::StaleObjectError' => :conflict, - 'ActiveRecord::RecordInvalid' => :unprocessable_entity, - 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::UnknownHttpMethod' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, + 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, + 'ActionDispatch::Http::Parameters::ParseError' => :bad_request, + 'ActionController::BadRequest' => :bad_request, + 'ActionController::ParameterMissing' => :bad_request, + 'Rack::QueryParser::ParameterTypeError' => :bad_request, + 'Rack::QueryParser::InvalidParameterError' => :bad_request, + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity } ``` @@ -486,8 +497,6 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `ActionDispatch::Callbacks.before` takes a block of code to run before the request. -* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`. - * `ActionDispatch::Callbacks.after` takes a block of code to run after the request. ### Configuring Action View @@ -536,6 +545,8 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `<span>` tag or not. This defaults to `true`. +* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`. + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: @@ -624,8 +635,6 @@ 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`. -* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns `false` (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `new_framework_defaults.rb` which sets the value to `false`. This file is *not* added when running `rails app:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code. - * `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. @@ -1181,7 +1190,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. -* `set_routes_reloader`: Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. +* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActiveSupport::Callbacks.to_run`. * `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`. @@ -1296,14 +1305,14 @@ evented file system monitor to detect changes when `config.cache_classes` is ```ruby group :development do - gem 'listen', '~> 3.0.4' + gem 'listen', '>= 3.0.5', '< 3.2' end ``` Otherwise, in every request Rails walks the application tree to check if anything has changed. -On Linux and Mac OS X no additional gems are needed, but some are required +On Linux and macOS no additional gems are needed, but some are required [for *BSD](https://github.com/guard/listen#on-bsd) and [for Windows](https://github.com/guard/listen#on-windows). diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 4f938f5deb..2f2962a3e6 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -15,7 +15,7 @@ After reading this guide, you will know: Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches. -As mentioned in [Rails +As mentioned in [Rails' README](https://github.com/rails/rails/blob/master/README.md), everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). -------------------------------------------------------------------------------- @@ -67,7 +67,7 @@ can expect it to be marked "invalid" as soon as it's reviewed. Sometimes, the line between 'bug' and 'feature' is a hard one to draw. Generally, a feature is anything that adds new behavior, while a bug is anything that causes incorrect behavior. Sometimes, -the core team will have to make a judgement call. That said, the distinction +the core team will have to make a judgment call. That said, the distinction generally just affects which release your patch will get in to; we love feature submissions! They just won't get backported to maintenance branches. @@ -98,13 +98,13 @@ Anything you can do to make bug reports more succinct or easier to reproduce hel ### Testing Patches -You can also help out by examining pull requests that have been submitted to Ruby on Rails via GitHub. To apply someone's changes you need first to create a dedicated branch: +You can also help out by examining pull requests that have been submitted to Ruby on Rails via GitHub. In order to apply someone's changes, you need to first create a dedicated branch: ```bash $ git checkout -b testing_branch ``` -Then you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails. +Then, you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails. ```bash $ git remote add JohnSmith https://github.com/JohnSmith/rails.git @@ -132,35 +132,24 @@ 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. -You can either open a pull request to [Rails](https://github.com/rails/rails) or -ask the [Rails core team](http://rubyonrails.org/community/#core) for commit access on -docrails if you contribute regularly. -Please do not open pull requests in docrails, if you'd like to get feedback on your -change, ask for it in [Rails](https://github.com/rails/rails) instead. - -Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation. - -If you are unsure of the documentation changes, you can create an issue in the [Rails](https://github.com/rails/rails/issues) issues tracker on GitHub. +To do so, open a pull request to [Rails](https://github.com/rails/rails) on GitHub. 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). -NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements. - NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. -WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. - Translating Rails Guides ------------------------ -We are happy to have people volunteer to translate the Rails guides into their own language. -If you want to translate the Rails guides in your own language, follows these steps: +We are happy to have people volunteer to translate the Rails guides. Just follow these steps: -* Fork the project (rails/rails). +* Fork https://github.com/rails/rails. * Add a source folder for your own language, for example: *guides/source/it-IT* for Italian. * Copy the contents of *guides/source* into your own language directory and translate them. * Do NOT translate the HTML files, as they are automatically generated. +Note that translations are not submitted to the Rails repository. As detailed above, your work happens in a fork. This is so because in practice documentation maintenance via patches is only sustainable in English. + To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT): ```bash @@ -270,33 +259,24 @@ The above are guidelines - please use your best judgment in using them. ### Benchmark Your Code -If your change has an impact on the performance of Rails, please use the -[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide -benchmark results for comparison. - -Here's an example of using benchmark-ips: - -```ruby -require 'benchmark/ips' - -Benchmark.ips do |x| - x.report('addition') { 1 + 2 } - x.report('addition with send') { 1.send(:+, 2) } -end -``` - -This will generate a report with the following information: - -``` -Calculating ------------------------------------- - addition 132.013k i/100ms - addition with send 125.413k i/100ms -------------------------------------------------- - addition 9.677M (± 1.7%) i/s - 48.449M - addition with send 6.794M (± 1.1%) i/s - 33.987M -``` - -Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information. +For changes that might have an impact on performance, please benchmark your +code and measure the impact. Please share the benchmark script you used as well +as the results. You should consider including this information in your commit +message, which allows future contributors to easily verify your findings and +determine if they are still relevant. (For example, future optimizations in the +Ruby VM might render certain optimizations unnecessary.) + +It is very easy to make an optimization that improves performance for a +specific scenario you care about but regresses on other common cases. +Therefore, you should test your change against a list of representative +scenarios. Ideally, they should be based on real-world scenarios extracted +from production applications. + +You can use the [benchmark template](https://github.com/rails/rails/blob/master/guides/bug_report_templates/benchmark.rb) +as a starting point. It includes the boilerplate code to setup a benchmark +using the [benchmark-ips](https://github.com/evanphx/benchmark-ips) gem. The +template is designed for testing relatively self-contained changes that can be +inlined into the script. ### Running Tests @@ -344,10 +324,12 @@ file. #### Testing Active Record -First, create the databases you'll need. For MySQL and PostgreSQL, -running the SQL statements `create database activerecord_unittest` and -`create database activerecord_unittest2` is sufficient. This is not -necessary for SQLite3. +First, create the databases you'll need. You can find a list of the required +table names, usernames, and passwords in `activerecord/test/config.example.yml`. + +For MySQL and PostgreSQL, running the SQL statements `create database +activerecord_unittest` and `create database activerecord_unittest2` is +sufficient. This is not necessary for SQLite3. This is how you run the Active Record test suite only for SQLite3: @@ -425,16 +407,6 @@ examples or multiple paragraphs. Otherwise, it's best to make a new paragraph. Some changes require the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. -### Sanity Check - -You should not be the only person who looks at the code before you submit it. -If you know someone else who uses Rails, try asking them if they'll check out -your work. If you don't know anyone else using Rails, try hopping into the IRC -room or posting about your idea to the rails-core mailing list. Doing this in -private before you push a patch out publicly is the "smoke test" for a patch: -if you can't convince one other developer of the beauty of your code, you’re -unlikely to convince the core team either. - ### Commit Your Changes When you're happy with the code on your computer, you need to commit the changes to Git: @@ -692,4 +664,4 @@ And then... think about your next contribution! Rails Contributors ------------------ -All contributions, either via master or docrails, get credit in [Rails Contributors](http://contributors.rubyonrails.org). +All contributions get credit in [Rails Contributors](http://contributors.rubyonrails.org). diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index df3003a6a8..58aab774b3 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -313,7 +313,7 @@ For example: ```bash => Booting Puma -=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000 +=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.4.0 (ruby 2.3.1-p112), codename: Owl Bowl Brawl @@ -445,11 +445,11 @@ then `backtrace` will supply the answer. --> #0 ArticlesController.index at /PathToProject/app/controllers/articles_controller.rb:8 #1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array) - at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/basic_implicit_render.rb:4 + at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/basic_implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb:181 + at /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb:181 #3 ActionController::Rendering.process_action(action, *args) - at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:30 + at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/rendering.rb:30 ... ``` @@ -461,7 +461,7 @@ context. ``` (byebug) frame 2 -[176, 185] in /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb +[176, 185] in /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb 176: # is the intended way to override action dispatching. 177: # 178: # Notice that the first argument is the method to be dispatched @@ -606,7 +606,6 @@ You can also inspect for an object method this way: @new_record = true @readonly = false @transaction_state = nil -@txn = nil ``` You can also use `display` to start watching variables. This is a good way of @@ -677,13 +676,13 @@ Ruby instruction to be executed -- in this case, Active Support's `week` method. ``` (byebug) step -[49, 58] in /PathToGems/activesupport-5.0.0/lib/active_support/core_ext/numeric/time.rb +[49, 58] in /PathToGems/activesupport-5.1.0/lib/active_support/core_ext/numeric/time.rb 49: 50: # Returns a Duration instance matching the number of weeks provided. 51: # 52: # 2.weeks # => 14 days 53: def weeks -=> 54: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) +=> 54: ActiveSupport::Duration.weeks(self) 55: end 56: alias :week :weeks 57: diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 20cd34c182..7ec038eb4d 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -46,7 +46,7 @@ $ cd rails The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests. -Install first SQLite3 and its development files for the `sqlite3` gem. Mac OS X +Install first SQLite3 and its development files for the `sqlite3` gem. On macOS users are done with: ```bash @@ -162,6 +162,10 @@ $ cd actionpack $ bundle exec ruby -Itest path/to/test.rb -n test_name ``` +### Railties Setup + +Some Railties tests depend on a JavaScript runtime environment, such as having [Node.js](https://nodejs.org/) installed. + ### Active Record Setup Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 2925fb4b58..2afef57fc2 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -194,6 +194,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.1 Release Notes + url: 5_1_release_notes.html + description: Release notes for Rails 5.1. + - name: Ruby on Rails 5.0 Release Notes url: 5_0_release_notes.html description: Release notes for Rails 5.0. diff --git a/guides/source/engines.md b/guides/source/engines.md index 0020112a1c..2276f348a1 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -14,6 +14,7 @@ After reading this guide, you will know: * How to build features for the engine. * How to hook the engine into an application. * How to override engine functionality in the application. +* Avoid loading Rails frameworks with Load and Configuration Hooks -------------------------------------------------------------------------------- @@ -59,7 +60,7 @@ only be enhancing it, rather than changing it drastically. To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or -[Forem](https://github.com/radar/forem), an engine that provides forum +[Thredded](https://github.com/thredded/thredded), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. @@ -1410,3 +1411,114 @@ module MyEngine end end ``` + +Active Support On Load Hooks +---------------------------- + +Active Support is the Ruby on Rails component responsible for providing Ruby language extensions, utilities, and other transversal utilities. + +Rails code can often be referenced on load of an application. Rails is responsible for the load order of these frameworks, so when you load frameworks, such as `ActiveRecord::Base`, prematurely you are violating an implicit contract your application has with Rails. Moreover, by loading code such as `ActiveRecord::Base` on boot of your application you are loading entire frameworks which may slow down your boot time and could cause conflicts with load order and boot of your application. + +On Load hooks are the API that allow you to hook into this initialization process without violating the load contract with Rails. This will also mitigate boot performance degradation and avoid conflicts. + +## What are `on_load` hooks? + +Since Ruby is a dynamic language, some code will cause different Rails frameworks to load. Take this snippet for instance: + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +This snippet means that when this file is loaded, it will encounter `ActiveRecord::Base`. This encounter causes Ruby to look for the definition of that constant and will require it. This causes the entire Active Record framework to be loaded on boot. + +`ActiveSupport.on_load` is a mechanism that can be used to defer the loading of code until it is actually needed. The snippet above can be changed to: + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } +``` + +This new snippet will only include `MyActiveRecordHelper` when `ActiveRecord::Base` is loaded. + +## How does it work? + +In the Rails framework these hooks are called when a specific library is loaded. For example, when `ActionController::Base` is loaded, the `:action_controller_base` hook is called. This means that all `ActiveSupport.on_load` calls with `:action_controller_base` hooks will be called in the context of `ActionController::Base` (that means `self` will be an `ActionController::Base`). + +## Modifying code to use `on_load` hooks + +Modifying code is generally straightforward. If you have a line of code that refers to a Rails framework such as `ActiveRecord::Base` you can wrap that code in an `on_load` hook. + +### Example 1 + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +becomes + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } # self refers to ActiveRecord::Base here, so we can simply #include +``` + +### Example 2 + +```ruby +ActionController::Base.prepend(MyActionControllerHelper) +``` + +becomes + +```ruby +ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper } # self refers to ActionController::Base here, so we can simply #prepend +``` + +### Example 3 + +```ruby +ActiveRecord::Base.include_root_in_json = true +``` + +becomes + +```ruby +ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } # self refers to ActiveRecord::Base here +``` + +## Available Hooks + +These are the hooks you can use in your own code. + +To hook into the initialization process of one of the following classes use the available hook. + +| Class | Available Hooks | +| --------------------------------- | ------------------------------------ | +| `ActionCable` | `action_cable` | +| `ActionController::API` | `action_controller_api` | +| `ActionController::API` | `action_controller` | +| `ActionController::Base` | `action_controller_base` | +| `ActionController::Base` | `action_controller` | +| `ActionController::TestCase` | `action_controller_test_case` | +| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` | +| `ActionMailer::Base` | `action_mailer` | +| `ActionMailer::TestCase` | `action_mailer_test_case` | +| `ActionView::Base` | `action_view` | +| `ActionView::TestCase` | `action_view_test_case` | +| `ActiveJob::Base` | `active_job` | +| `ActiveJob::TestCase` | `active_job_test_case` | +| `ActiveRecord::Base` | `active_record` | +| `ActiveSupport::TestCase` | `active_support_test_case` | +| `i18n` | `i18n` | + +## 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. + +| 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. | + +### Example + +`config.before_configuration { puts 'I am called before any initializers' }` diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 048fe190e8..f46f1648b3 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -164,7 +164,7 @@ make it easier for users to click the inputs. Other form controls worth mentioning are textareas, password fields, hidden fields, search fields, telephone fields, date fields, time fields, -color fields, datetime fields, datetime-local fields, month fields, week fields, +color fields, datetime-local fields, month fields, week fields, URL fields, email fields, number fields and range fields: ```erb @@ -438,8 +438,6 @@ output: Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option. -TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings. - WARNING: When `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. You can add arbitrary attributes to the options using hashes: @@ -533,7 +531,7 @@ To leverage time zone support in Rails, you have to ask your users what time zon <%= time_zone_select(:person, :time_zone) %> ``` -There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the API documentation to learn about the possible arguments for these two methods. +There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-time_zone_options_for_select) to learn about the possible arguments for these two methods. Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/stefanpenner/country_select). When using this, be aware that the exclusion or inclusion of certain names from the list can be somewhat controversial (and was the reason this functionality was extracted from Rails). @@ -879,7 +877,7 @@ Active Record provides model level support via the `accepts_nested_attributes_fo ```ruby class Person < ApplicationRecord - has_many :addresses + has_many :addresses, inverse_of: :person accepts_nested_attributes_for :addresses end diff --git a/guides/source/generators.md b/guides/source/generators.md index 32bbdc554a..d4ed2355d4 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -96,7 +96,7 @@ This is the generator just created: ```ruby class InitializerGenerator < Rails::Generators::NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) end ``` @@ -122,7 +122,7 @@ And now let's change the generator to copy this template when invoked: ```ruby class InitializerGenerator < Rails::Generators::NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_initializer_file copy_file "initializer.rb", "config/initializers/#{file_name}.rb" @@ -208,7 +208,15 @@ $ bin/rails generate scaffold User name:string Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. -Our first customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following: +If we want to avoid generating the default `app/assets/stylesheets/scaffolds.scss` file when scaffolding a new resource we can disable `scaffold_stylesheet`: + +```ruby + config.generators do |g| + g.scaffold_stylesheet false + end +``` + +The next customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -418,7 +426,7 @@ Fallbacks allow your generators to have a single responsibility, increasing code Application Templates --------------------- -Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html). +Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred to as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html). ```ruby gem "rspec-rails", group: "test" @@ -451,6 +459,26 @@ $ rails new thud -m https://gist.github.com/radar/722911/raw/ Whilst the final section of this guide doesn't cover how to generate the most awesome template known to man, it will take you through the methods available at your disposal so that you can develop it yourself. These same methods are also available for generators. +Adding Command Line Arguments +----------------------------- +Rails generators can be easily modified to accept custom command line arguments. This functionality comes from [Thor](http://www.rubydoc.info/github/erikhuda/thor/master/Thor/Base/ClassMethods#class_option-instance_method): + +``` +class_option :scope, type: :string, default: 'read_products' +``` + +Now our generator can be invoked as follows: + +```bash +rails generate initializer --scope write_products +``` + +The command line arguments are accessed through the `options` method inside the generator class. e.g: + +```ruby +@scope = options['scope'] +``` + Generator methods ----------------- diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 31d5c4f71d..21bd4a3d7a 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -20,16 +20,7 @@ Guide Assumptions This guide is designed for beginners who want to get started with a Rails application from scratch. It does not assume that you have any prior experience -with Rails. However, to get the most out of it, you need to have some -prerequisites installed: - -* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer. -* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you - are using Windows. -* The [RubyGems](https://rubygems.org) packaging system, which is installed with - Ruby by default. To learn more about RubyGems, please read the - [RubyGems Guides](http://guides.rubygems.org). -* A working installation of the [SQLite3 Database](https://www.sqlite.org). +with Rails. Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning @@ -46,7 +37,7 @@ development with Rails. What is Rails? -------------- -Rails is a web application development framework written in the Ruby language. +Rails is a web application development framework written in the Ruby programming language. It is designed to make programming web applications easier by making assumptions about what every developer needs to get started. It allows you to write less code while accomplishing more than many other languages and frameworks. @@ -86,7 +77,10 @@ your prompt will look something like `c:\source_code>` ### Installing Rails -Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose +Before you install Rails, you should check to make sure that your system has the +proper prerequisites installed. These include Ruby and SQLite3. + +Open up a command line prompt. On macOS open Terminal.app, on Windows choose "Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: @@ -96,12 +90,19 @@ $ ruby -v ruby 2.3.1p112 ``` +Rails requires Ruby version 2.2.2 or later. If the version number returned is +less than that number, you'll need to install a fresh copy of Ruby. + TIP: A number of tools exist to help you quickly install Ruby and Ruby on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), -while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp). +while macOS users can use [Tokaido](https://github.com/tokaido/tokaidoapp). For more installation methods for most Operating Systems take a look at [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). +If you are working on Windows, you should also install the +[Ruby Installer Development Kit](http://rubyinstaller.org/downloads/). + +You will also need an installation of the SQLite3 database. Many popular UNIX-like OSes ship with an acceptable version of SQLite3. On Windows, if you installed Rails through Rails Installer, you already have SQLite installed. Others can find installation instructions @@ -127,7 +128,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 5.0.0", you are ready to continue. +If it says something like "Rails 5.1.1", you are ready to continue. ### Creating the Blog Application @@ -182,7 +183,7 @@ of the files and folders that Rails created by default: |test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).| |tmp/|Temporary files (like cache and pid files).| |vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.| -|.gitignore|This file tells git which files (or patterns) it should ignore. See [Github - Ignoring files](https://help.github.com/articles/ignoring-files) for more info about ignoring files. +|.gitignore|This file tells git which files (or patterns) it should ignore. See [GitHub - Ignoring files](https://help.github.com/articles/ignoring-files) for more info about ignoring files. Hello, Rails! ------------- @@ -206,8 +207,8 @@ folder directly to the Ruby interpreter e.g. `ruby bin\rails server`. TIP: Compiling CoffeeScript and JavaScript asset compression requires you have a JavaScript runtime available on your system, in the absence of a runtime you will see an `execjs` error during asset compilation. -Usually Mac OS X and Windows come with a JavaScript runtime installed. -Rails adds the `therubyracer` gem to the generated `Gemfile` in a +Usually macOS and Windows come with a JavaScript runtime installed. +Rails adds the `mini_racer` gem to the generated `Gemfile` in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to the `Gemfile` in apps generated under JRuby. You can investigate @@ -221,7 +222,7 @@ your application in action, open a browser window and navigate to TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt -cursor again. For most UNIX-like systems including Mac OS X this will be a +cursor again. For most UNIX-like systems including macOS this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server. @@ -474,7 +475,7 @@ one here because the `ArticlesController` inherits from `ApplicationController`. The next part of the message contains `request.formats` which specifies the format of template to be served in response. It is set to `text/html` as we requested this page via browser, so Rails is looking for an HTML template. -`request.variants` specifies what kind of physical devices would be served by +`request.variant` specifies what kind of physical devices would be served by the response and helps Rails determine which template to use in the response. It is empty because no information has been provided. @@ -827,7 +828,7 @@ NOTE: A frequent practice is to place the standard CRUD actions in each controller in the following order: `index`, `show`, `new`, `edit`, `create`, `update` and `destroy`. You may use any order you choose, but keep in mind that these are public methods; as mentioned earlier in this guide, they must be placed -before any private or protected method in the controller in order to work. +before declaring `private` visibility in the controller. Given that, let's add the `show` action, as follows: @@ -1157,7 +1158,7 @@ it look as follows: ```html+erb <h1>Edit article</h1> -<%= form_for :article, url: article_path(@article), method: :patch do |f| %> +<%= form_for(@article) do |f| %> <% if @article.errors.any? %> <div id="error_explanation"> @@ -1195,14 +1196,15 @@ it look as follows: This time we point the form to the `update` action, which is not defined yet but will be very soon. -The `method: :patch` option tells Rails that we want this form to be submitted +Passing the article object to the method, will automagically create url for submitting the edited article form. +This option tells Rails that we want this form to be submitted via the `PATCH` HTTP method which is the HTTP method you're expected to use to **update** resources according to the REST protocol. The first parameter of `form_for` can be an object, say, `@article` which would cause the helper to fill in the form with the fields of the object. Passing in a symbol (`:article`) with the same name as the instance variable (`@article`) -also automagically leads to the same behavior. This is what is happening here. +also automagically leads to the same behavior. More details can be found in [form_for documentation] (http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for). @@ -1655,8 +1657,8 @@ This creates five files and one empty directory: | app/views/comments/ | Views of the controller are stored here | | test/controllers/comments_controller_test.rb | The test for the controller | | app/helpers/comments_helper.rb | A view helper file | -| app/assets/javascripts/comment.coffee | CoffeeScript for the controller | -| app/assets/stylesheets/comment.scss | Cascading style sheet for the controller | +| app/assets/javascripts/comments.coffee | CoffeeScript for the controller | +| app/assets/stylesheets/comments.scss | Cascading style sheet for the controller | Like with any blog, our readers will create their comments directly after reading the article, and once they have added their comment, will be sent back diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 887774961a..6c8706bc13 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -72,11 +72,13 @@ I18n.l Time.now There are also attribute readers and writers for the following attributes: ```ruby -load_path # Announce your custom translation files -locale # Get and set the current locale -default_locale # Get and set the default locale -exception_handler # Use a different exception_handler -backend # Use a different backend +load_path # Announce your custom translation files +locale # Get and set the current locale +default_locale # Get and set the default locale +available_locales # Whitelist locales available for the application +enforce_available_locales # Enforce locale whitelisting (true or false) +exception_handler # Use a different exception_handler +backend # Use a different backend ``` So, let's internationalize a simple Rails application from the ground up in the next chapters! @@ -124,6 +126,9 @@ The load path must be specified before any translations are looked up. To change # Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] +# Whitelist locales available for the application +I18n.available_locales = [:en, :pt] + # Set default locale to something other than :en I18n.default_locale = :pt ``` @@ -166,7 +171,7 @@ def set_locale I18n.locale = extract_locale_from_tld || I18n.default_locale end -# Get locale from top-level domain or return nil if such locale is not available +# Get locale from top-level domain or return +nil+ if such locale is not available # You have to put something like: # 127.0.0.1 application.com # 127.0.0.1 application.it @@ -404,6 +409,35 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) +If your translations are stored in YAML files, certain keys must be escaped. They are: + +* true, on, yes +* false, off, no + +Examples: + +```erb +# config/locales/en.yml +en: + success: + 'true': 'True!' + 'on': 'On!' + 'false': 'False!' + failure: + true: 'True!' + off: 'Off!' + false: 'False!' +``` + +```ruby +I18n.t 'success.true' # => 'True!' +I18n.t 'success.on' # => 'On!' +I18n.t 'success.false' # => 'False!' +I18n.t 'failure.false' # => Translation Missing +I18n.t 'failure.off' # => Translation Missing +I18n.t 'failure.true' # => Translation Missing +``` + ### Passing Variables to Translations One key consideration for successfully internationalizing an application is to @@ -667,12 +701,13 @@ end ### Pluralization -In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. +In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar), [Japanese](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ja), [Russian](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://cldr.unicode.org/index/cldr-spec/plural-rules). Thus, the I18n API provides a flexible pluralization feature. The `:count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR: ```ruby I18n.backend.store_translations :en, inbox: { + zero: 'no messages', # optional one: 'one message', other: '%{count} messages' } @@ -681,15 +716,20 @@ I18n.translate :inbox, count: 2 I18n.translate :inbox, count: 1 # => 'one message' + +I18n.translate :inbox, count: 0 +# => 'no messages' ``` The algorithm for pluralizations in `:en` is as simple as: ```ruby -entry[count == 1 ? 0 : 1] +lookup_key = :zero if count == 0 && entry.has_key?(:zero) +lookup_key ||= count == 1 ? :one : :other +entry[lookup_key] ``` -I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero). +The translation denoted as `:one` is regarded as singular, and the `:other` is used as plural. If the count is zero, and a `:zero` entry is present, then it will be used instead of `:other`. If the lookup for the key does not return a Hash suitable for pluralization, an `I18n::InvalidPluralizationData` exception is raised. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 57ed35d0d8..3ea156c6fe 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -74,7 +74,7 @@ This file is as follows: ```ruby #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' ``` @@ -86,7 +86,7 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot `config/boot.rb` contains: ```ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. ``` @@ -131,7 +131,7 @@ Once `config/boot.rb` has finished, the next file that is required is `ARGV` array simply contains `server` which will be passed over: ```ruby -ARGV << '--help' if ARGV.empty? +require "rails/command" aliases = { "g" => "generate", @@ -146,33 +146,37 @@ aliases = { command = ARGV.shift command = aliases[command] || command -require 'rails/commands/commands_tasks' - -Rails::CommandsTasks.new(ARGV).run_command!(command) +Rails::Command.invoke command, ARGV ``` -TIP: As you can see, an empty ARGV list will make Rails show the help -snippet. - If we had used `s` rather than `server`, Rails would have used the `aliases` defined here to find the matching command. -### `rails/commands/commands_tasks.rb` +### `rails/command.rb` -When one types a valid Rails command, `run_command!` a method of the same name -is called. If Rails doesn't recognize the command, it tries to run a Rake task -of the same name. +When one types a Rails command, `invoke` tries to lookup a command for the given +namespace and executing the command if found. -```ruby -COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help) +If Rails doesn't recognize the command, it hands the reins over to Rake +to run a task of the same name. -def run_command!(command) - command = parse_command(command) +As shown, `Rails::Command` displays the help output automatically if the `args` +are empty. - if COMMAND_WHITELIST.include?(command) - send(command) - else - run_rake_task(command) +```ruby +module Rails::Command + class << self + def invoke(namespace, args = [], **config) + namespace = namespace.to_s + namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) + namespace = "version" if %w( -v --version ).include? namespace + + if command = find_by_namespace(namespace) + command.perform(namespace, args, config) + else + find_by_namespace("rake").perform(namespace, args, config) + end + end end end ``` @@ -180,53 +184,39 @@ end With the `server` command, Rails will further run the following code: ```ruby -def set_application_directory! - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru")) -end - -def server - set_application_directory! - require_command!("server") - - Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. - require APP_PATH - Dir.chdir(Rails.application.root) - server.start +module Rails + module Command + class ServerCommand < Base # :nodoc: + def perform + set_application_directory! + + Rails::Server.new.tap do |server| + # Require application after server sets environment to propagate + # the --environment option. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start + end + end + end end end - -def require_command!(command) - require "rails/commands/#{command}" -end ``` This file will change into the Rails root directory (a path two directories up from `APP_PATH` which points at `config/application.rb`), but only if the -`config.ru` file isn't found. This then requires `rails/commands/server` which -sets up the `Rails::Server` class. - -```ruby -require 'fileutils' -require 'optparse' -require 'action_dispatch' -require 'rails' - -module Rails - class Server < ::Rack::Server -``` - -`fileutils` and `optparse` are standard Ruby libraries which provide helper functions for working with files and parsing options. +`config.ru` file isn't found. This then starts up the `Rails::Server` class. ### `actionpack/lib/action_dispatch.rb` Action Dispatch is the routing component of the Rails framework. It adds functionality like routing, session, and common middlewares. -### `rails/commands/server.rb` +### `rails/commands/server/server_command.rb` -The `Rails::Server` class is defined in this file by inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`: +The `Rails::Server` class is defined in this file by inheriting from +`Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` +method in `rails/commands/server/server_command.rb`: ```ruby def initialize(*) @@ -252,7 +242,10 @@ end In this case, `options` will be `nil` so nothing happens in this method. -After `super` has finished in `Rack::Server`, we jump back to `rails/commands/server.rb`. At this point, `set_environment` is called within the context of the `Rails::Server` object and this method doesn't appear to do much at first glance: +After `super` has finished in `Rack::Server`, we jump back to +`rails/commands/server/server_command.rb`. At this point, `set_environment` +is called within the context of the `Rails::Server` object and this method +doesn't appear to do much at first glance: ```ruby def set_environment @@ -289,17 +282,15 @@ With the `default_options` set to this: ```ruby def default_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - :environment => environment, - :pid => nil, - :Port => 9292, - :Host => default_host, - :AccessLog => [], - :config => "config.ru" - } + super.merge( + Port: ENV.fetch("PORT", 3000).to_i, + Host: ENV.fetch("HOST", "localhost").dup, + DoNotReverseLookup: true, + environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, + daemonize: false, + caching: nil, + pid: Options::DEFAULT_PID_PATH, + restart_cmd: restart_command) end ``` @@ -311,22 +302,25 @@ def opt_parser end ``` -The class **is** defined in `Rack::Server`, but is overwritten in `Rails::Server` to take different arguments. Its `parse!` method begins like this: +The class **is** defined in `Rack::Server`, but is overwritten in +`Rails::Server` to take different arguments. Its `parse!` method looks +like this: ```ruby def parse!(args) args, options = args.dup, {} - opt_parser = OptionParser.new do |opts| - opts.banner = "Usage: rails server [puma, thin, etc] [options]" - opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } - ... + option_parser(options).parse! args + + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift + options +end ``` This method will set up keys for the `options` which Rails will then be able to use to determine how its server should run. After `initialize` -has finished, we jump back into `rails/server` where `APP_PATH` (which was +has finished, we jump back into the server command where `APP_PATH` (which was set earlier) is required. ### `config/application` @@ -345,6 +339,7 @@ def start print_boot_information trap(:INT) { exit } create_tmp_directories + setup_dev_caching log_to_stdout if options[:log_stdout] super @@ -352,7 +347,6 @@ def start end private - def print_boot_information ... puts "=> Run `rails server -h` for more startup options" @@ -364,21 +358,30 @@ private end end + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + def log_to_stdout wrapped_app # touch the app so the logger is set up - - console = ActiveSupport::Logger.new($stdout) + + console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level - - Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end end ``` This is where the first output of the Rails initialization happens. This method creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the process. As we can see from the code here, it will create the `tmp/cache`, -`tmp/pids`, and `tmp/sockets` directories. It then calls `wrapped_app` which is +`tmp/pids`, and `tmp/sockets` directories. It then enables caching in development +if `rails server` is called with `--dev-caching`. Finally, it calls `wrapped_app` which is responsible for creating the Rack app, before creating and assigning an instance of `ActiveSupport::Logger`. @@ -538,7 +541,7 @@ require "rails" sprockets/railtie ).each do |railtie| begin - require "#{railtie}" + require railtie rescue LoadError end end diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb index 547abcbc19..63eeb007d7 100644 --- a/guides/source/kindle/rails_guides.opf.erb +++ b/guides/source/kindle/rails_guides.opf.erb @@ -5,7 +5,7 @@ <meta name="cover" content="cover" /> <dc-metadata xmlns:dc="http://purl.org/dc/elements/1.1/"> - <dc:title>Ruby on Rails Guides (<%= @version %>)</dc:title> + <dc:title>Ruby on Rails Guides (<%= @version || "master@#{@edge[0, 7]}" %>)</dc:title> <dc:language>en-us</dc:language> <dc:creator>Ruby on Rails</dc:creator> diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 943fd3fd7f..bb50761b30 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -88,7 +88,7 @@ <div id="container"> <div class="wrapper"> <div id="mainCol"> - <%= yield.html_safe %> + <%= yield %> <h3>Feedback</h3> <p> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 7e4ec5ba7e..caa3d21d23 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -411,6 +411,8 @@ render formats: :xml render formats: [:json, :xml] ``` +If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised. + #### Finding Layouts To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions. @@ -766,7 +768,7 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th #### Linking to Feeds with the `auto_discovery_link_tag` -The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag: +The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS, Atom, or JSON feeds. It takes the type of the link (`:rss`, `:atom`, or `:json`), a hash of options that are passed through to url_for, and a hash of options for the tag: ```erb <%= auto_discovery_link_tag(:rss, {action: "feed"}, @@ -1082,7 +1084,7 @@ definitions for several similar resources: * `shared/_search_filters.html.erb` ```html+erb - <%= form_for(@q) do |f| %> + <%= form_for(search) do |f| %> <h1>Search form:</h1> <fieldset> <%= yield f %> @@ -1155,7 +1157,7 @@ To pass a local variable to a partial in only specific cases use the `local_assi <%= render article, full: true %> ``` -* `_articles.html.erb` +* `_article.html.erb` ```erb <h2><%= article.title %></h2> @@ -1280,7 +1282,7 @@ When rendering collections it is also possible to use the `:layout` option: <%= render partial: "product", collection: @products, layout: "special_layout" %> ``` -The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they do within the partial. +The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they are within the partial. ### Using Nested Layouts diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md index 7ced3eab1c..1d6a4edb5b 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -44,7 +44,7 @@ from. In special situations, where someone from the Core Team agrees to support more series, they are included in the list of supported series. -**Currently included series:** `5.0.Z`, `4.2.Z`. +**Currently included series:** `5.1.Z`. Security Issues --------------- @@ -59,7 +59,7 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that security releases are easy to upgrade to if you're running the latest version of Rails. -**Currently included series:** `5.0.Z`, `4.2.Z`. +**Currently included series:** `5.1.Z`, `5.0.Z`. Severe Security Issues ---------------------- @@ -68,7 +68,7 @@ For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. -**Currently included series:** `5.0.Z`, `4.2.Z`. +**Currently included series:** `5.1.Z`, `5.0.Z`, `4.2.Z`. Unsupported Release Series -------------------------- diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md deleted file mode 100644 index 71efa4b0d0..0000000000 --- a/guides/source/nested_model_forms.md +++ /dev/null @@ -1,230 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Rails Nested Model Forms -======================== - -Creating a form for a model _and_ its associations can become quite tedious. Therefore Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations. - -After reading this guide, you will know: - -* do stuff. - --------------------------------------------------------------------------------- - -NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it's **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/). - - -Model setup ------------ - -To be able to use the nested model functionality in your forms, the model will need to support some basic operations. - -First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be built. - -If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded. - -Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the `:address` attribute, the `fields_for` form helper will look for a method on the Person instance named `address_attributes=`. - -### ActiveRecord::Base model - -For an ActiveRecord::Base model and association this writer method is commonly defined with the `accepts_nested_attributes_for` class method: - -#### has_one - -```ruby -class Person < ApplicationRecord - has_one :address - accepts_nested_attributes_for :address -end -``` - -#### belongs_to - -```ruby -class Person < ApplicationRecord - belongs_to :firm - accepts_nested_attributes_for :firm -end -``` - -#### has_many / has_and_belongs_to_many - -```ruby -class Person < ApplicationRecord - has_many :projects - accepts_nested_attributes_for :projects -end -``` - -NOTE: For greater detail on associations see [Active Record Associations](association_basics.html). -For a complete reference on associations please visit the API documentation for [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). - -### Custom model - -As you might have inflected from this explanation, you _don't_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behavior: - -#### Single associated object - -```ruby -class Person - def address - Address.new - end - - def address_attributes=(attributes) - # ... - end -end -``` - -#### Association collection - -```ruby -class Person - def projects - [Project.new, Project.new] - end - - def projects_attributes=(attributes) - # ... - end -end -``` - -NOTE: See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model. - -Views ------ - -### Controller code - -A nested model form will _only_ be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first. - -Consider the following typical RESTful controller which will prepare a new Person instance and its `address` and `projects` associations before rendering the `new` template: - -```ruby -class PeopleController < ApplicationController - def new - @person = Person.new - @person.build_address - 2.times { @person.projects.build } - end - - def create - @person = Person.new(params[:person]) - if @person.save - # ... - end - end -end -``` - -NOTE: Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an `after_initialize` callback which is a good way to refactor this. - -### Form code - -Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form. - -#### Standard form - -Start out with a regular RESTful form: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> -<% end %> -``` - -This will generate the following html: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> -</form> -``` - -#### Nested form for a single associated object - -Now add a nested form for the `address` association: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> - - <%= f.fields_for :address do |af| %> - <%= af.text_field :street %> - <% end %> -<% end %> -``` - -This generates: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - - <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" /> -</form> -``` - -Notice that `fields_for` recognized the `address` as an association for which a nested model form should be built by the way it has namespaced the `name` attribute. - -When this form is posted the Rails parameter parser will construct a hash like the following: - -```ruby -{ - "person" => { - "name" => "Eloy Duran", - "address_attributes" => { - "street" => "Nieuwe Prinsengracht" - } - } -} -``` - -That's it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved. - -#### Nested form for a collection of associated objects - -The form code for an association collection is pretty similar to that of a single associated object: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> - - <%= f.fields_for :projects do |pf| %> - <%= pf.text_field :name %> - <% end %> -<% end %> -``` - -Which generates: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - - <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" /> - <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" /> -</form> -``` - -As you can see it has generated 2 `project name` inputs, one for each new `project` that was built in the controller's `new` action. Only this time the `name` attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as: - -```ruby -{ - "person" => { - "name" => "Eloy Duran", - "projects_attributes" => { - "0" => { "name" => "Project 1" }, - "1" => { "name" => "Project 2" } - } - } -} -``` - -You can basically see the `projects_attributes` hash as an array of attribute hashes, one for each model instance. - -NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep. - -TIP: You _can_ however pass an array to the writer method generated by `accepts_nested_attributes_for` if you're using plain Ruby or some other API access. See (TODO) for more info and example. diff --git a/guides/source/profiling.md b/guides/source/profiling.md deleted file mode 100644 index ce093f78ba..0000000000 --- a/guides/source/profiling.md +++ /dev/null @@ -1,16 +0,0 @@ -*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -A Guide to Profiling Rails Applications -======================================= - -This guide covers built-in mechanisms in Rails for profiling your application. - -After reading this guide, you will know: - -* Rails profiling terminology. -* How to write benchmark tests for your application. -* Other benchmarking approaches and plugins. - --------------------------------------------------------------------------------- - - diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 3e99ee7021..e087834a2f 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -277,6 +277,6 @@ relative paths to your template's location. ```ruby def source_paths - [File.expand_path(File.dirname(__FILE__))] + [__dir__] end ``` diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index ed935e1008..cef8450ee4 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -20,9 +20,9 @@ Introduction to Rack Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -* [Rack API Documentation](http://rack.github.io/) - -Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below. +Explaining how Rack works is not really in the scope of this guide. In case you +are not familiar with Rack's basics, you should check out the [Resources](#resources) +section below. Rails on Rack ------------- @@ -74,7 +74,7 @@ And start the server: $ rackup config.ru ``` -To find out more about different `rackup` options: +To find out more about different `rackup` options, you can run: ```bash $ rackup --help @@ -89,7 +89,8 @@ Action Dispatcher Middleware Stack Many of Action Dispatcher's internal components are implemented as Rack middlewares. `Rails::Application` uses `ActionDispatch::MiddlewareStack` to combine various internal and external middlewares to form a complete Rails Rack application. -NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, but built for better flexibility and more features to meet Rails' requirements. +NOTE: `ActionDispatch::MiddlewareStack` is Rails' equivalent of `Rack::Builder`, +but is built for better flexibility and more features to meet Rails' requirements. ### Inspecting Middleware Stack @@ -109,11 +110,12 @@ use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId +use ActionDispatch::RemoteIp +use Sprockets::Rails::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions -use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending @@ -123,7 +125,7 @@ use ActionDispatch::Flash use Rack::Head use Rack::ConditionalGet use Rack::ETag -run Rails.application.routes +run MyApp.application.routes ``` The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below. @@ -181,7 +183,6 @@ $ bin/rails middleware (in /Users/lifo/Rails/blog) use ActionDispatch::Static use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8> -use Rack::Runtime ... run Rails.application.routes ``` @@ -238,6 +239,14 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#request_id` method. +**`ActionDispatch::RemoteIp`** + +* Checks for IP spoofing attacks. + +**`Sprockets::Rails::QuietAssets`** + +* Suppresses logger output for asset requests. + **`Rails::Rack::Logger`** * Notifies the logs that the request has began. After request is complete, flushes all the logs. @@ -250,10 +259,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Responsible for logging exceptions and showing a debugging page in case the request is local. -**`ActionDispatch::RemoteIp`** - -* Checks for IP spoofing attacks. - **`ActionDispatch::Reloader`** * Provides prepare and cleanup callbacks, intended to assist with code reloading during development. diff --git a/guides/source/routing.md b/guides/source/routing.md index 937e313663..f7dbbc510e 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -142,16 +142,17 @@ Sometimes, you have a resource that clients always look up without referencing a get 'profile', to: 'users#show' ``` -Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action but you must also specify the `controller:` to use: +Passing a `String` to `to:` will expect a `controller#action` format. When using a `Symbol`, the `to:` option should be replaced with `action:`. When using a `String` without a `#`, the `to:` option should be replaced with `controller:`: ```ruby -get 'profile', to: :show, controller: 'users' +get 'profile', action: :show, controller: 'users' ``` This resourceful route: ```ruby resource :geocoder +resolve('Geocoder') { [:geocoder] } ``` creates six different routes in your application, all mapping to the `Geocoders` controller: @@ -175,14 +176,6 @@ A singular resourceful route generates these helpers: As with plural resources, the same helpers ending in `_url` will also include the host, port and path prefix. -WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) prevents `form_for` from working automatically with singular resources. As a workaround, specify the URL for the form directly, like so: - -```ruby -form_for @geocoder, url: geocoder_path do |f| - -# snippet for brevity -``` - ### Controller Namespaces and Routing You may wish to organize groups of controllers under a namespace. Most commonly, you might group a number of administrative controllers under an `Admin::` namespace. You would place these controllers under the `app/controllers/admin` directory, and you can group them together in your router: @@ -545,7 +538,7 @@ TIP: If you find yourself adding many extra actions to a resourceful route, it's Non-Resourceful Routes ---------------------- -In addition to resource routing, Rails has powerful support for routing arbitrary URLs to actions. Here, you don't get groups of routes automatically generated by resourceful routing. Instead, you set up each route within your application separately. +In addition to resource routing, Rails has powerful support for routing arbitrary URLs to actions. Here, you don't get groups of routes automatically generated by resourceful routing. Instead, you set up each route separately within your application. While you should usually use resourceful routing, there are still many places where the simpler routing is more appropriate. There's no need to try to shoehorn every last piece of your application into a resourceful framework if that's not a good fit. @@ -603,6 +596,14 @@ get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' } Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`. +You can also use `defaults` in a block format to define the defaults for multiple items: + +```ruby +defaults format: :json do + resources :photos +end +``` + NOTE: You cannot override defaults via query parameters - this is for security reasons. The only defaults that can be overridden are dynamic segments via substitution in the URL path. ### Naming Routes diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 50866350f8..de63e193f4 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -50,6 +50,48 @@ Use the same inline formatting as regular text: ##### The `:content_type` Option ``` +Linking to the API +------------------ + +Links to the API (`api.rubyonrails.org`) are processed by the guides generator in the following manner: + +Links that include a release tag are left untouched. For example + +``` +http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html +``` + +is not modified. + +Please use these in release notes, since they should point to the corresponding version no matter the target being generated. + +If the link does not include a release tag and edge guides are being generated, the domain is replaced by `edgeapi.rubyonrails.org`. For example, + +``` +http://api.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +becomes + +``` +http://edgeapi.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +If the link does not include a release tag and release guides are being generated, the Rails version is injected. For example, if we are generating the guides for v5.1.0 the link + +``` +http://api.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +becomes + +``` +http://api.rubyonrails.org/v5.1.0/classes/ActionDispatch/Response.html +``` + +Please don't link to `edgeapi.rubyonrails.org` manually. + + API Documentation Guidelines ---------------------------- @@ -97,8 +139,6 @@ By default, guides that have not been modified are not processed, so `ONLY` is r To force processing all the guides, pass `ALL=1`. -It is also recommended that you work with `WARNINGS=1`. This detects duplicate IDs and warns about broken internal links. - If you want to generate guides in a language other than English, you can keep them in a separate directory under `source` (eg. `source/es`) and use the `GUIDES_LANGUAGE` environment variable: ``` diff --git a/guides/source/security.md b/guides/source/security.md index aea9728c10..f69a0c72b0 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -95,16 +95,23 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves * The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie. -However, since Rails 4, the default store is EncryptedCookieStore. With -EncryptedCookieStore the session is encrypted before being stored in a cookie. -This prevents the user from accessing and tampering the content of the cookie. -Thus the session becomes a more secure place to store data. The encryption is -done using a server-side secret key `secrets.secret_key_base` stored in -`config/secrets.yml`. +In Rails 4, encrypted cookies through AES in CBC mode with HMAC using SHA1 for +verification was introduced. This prevents the user from accessing and tampering +the content of the cookie. Thus the session becomes a more secure place to store +data. The encryption is performed using a server-side `secrets.secret_key_base`. +Two salts are used when deriving keys for encryption and verification. These +salts are set via the `config.action_dispatch.encrypted_cookie_salt` and +`config.action_dispatch.encrypted_signed_cookie_salt` configuration values. -That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rails secret` instead_. +Rails 5.2 uses AES-GCM for the encryption which couples authentication +and encryption in one faster step and produces shorter ciphertexts. -`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: +Encrypted cookies are automatically upgraded if the +`config.action_dispatch.use_authenticated_cookie_encryption` is enabled. + +_Do not use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters! Instead use `rails secret` to generate secret keys!_ + +Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: development: secret_key_base: a75d... @@ -212,7 +219,7 @@ CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less th NOTE: _First, as is required by the W3C, use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF._ -The HTTP protocol basically provides two main types of requests - GET and POST (and more, but they are not supported by most browsers). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST: +The HTTP protocol basically provides two main types of requests - GET and POST (DELETE, PUT, and PATCH should be used like POST). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST: **Use GET if:** @@ -224,7 +231,7 @@ The HTTP protocol basically provides two main types of requests - GET and POST ( * The interaction _changes the state_ of the resource in a way that the user would perceive (e.g., a subscription to a service), or * The user is _held accountable for the results_ of the interaction. -If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier. +If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Some legacy web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle these cases. _POST requests can be sent automatically, too_. In this example, the link www.harmless.com is shown as the destination in the browser's status bar. But it has actually dynamically created a new form that sends a POST request. @@ -257,13 +264,12 @@ protect_from_forgery with: :exception This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown. -NOTE: By default, Rails includes jQuery and an [unobtrusive scripting adapter for -jQuery](https://github.com/rails/jquery-ujs), which adds a header called -`X-CSRF-Token` on every non-GET Ajax call made by jQuery with the security token. -Without this header, non-GET Ajax requests won't be accepted by Rails. When using -another library to make Ajax calls, it is necessary to add the security token as -a default header for Ajax calls in your library. To get the token, have a look at -`<meta name='csrf-token' content='THE-TOKEN'>` tag printed by +NOTE: By default, Rails includes an [unobtrusive scripting adapter](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts), +which adds a header called `X-CSRF-Token` with the security token on every non-GET +Ajax call. Without this header, non-GET Ajax requests won't be accepted by Rails. +When using another library to make Ajax calls, it is necessary to add the security +token as a default header for Ajax calls in your library. To get the token, have +a look at `<meta name='csrf-token' content='THE-TOKEN'>` tag printed by `<%= csrf_meta_tags %>` in your application view. It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself: @@ -357,7 +363,7 @@ send_file('/var/www/uploads/' + params[:filename]) Simply pass a file name like "../../../etc/passwd" to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_: ```ruby -basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files')) +basename = File.expand_path('../../files', __dir__) filename = File.expand_path(File.join(basename, @file.public_filename)) raise if basename != File.expand_path(File.join(File.dirname(filename), '../../../')) @@ -377,7 +383,7 @@ In 2007 there was the first tailor-made trojan which stole information from an I Having one single place in the admin interface or Intranet, where the input has not been sanitized, makes the entire application vulnerable. Possible exploits include stealing the privileged administrator's cookie, injecting an iframe to steal the administrator's password or installing malicious software through browser security holes to take over the administrator's computer. -Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface. +Refer to the Injection section for countermeasures against XSS. **CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface. @@ -615,7 +621,7 @@ The two dashes start a comment ignoring everything after it. So the query return Usually a web application includes access control. The user enters their login credentials and the web application tries to find the matching record in the users table. The application grants access when it finds a record. However, an attacker may possibly bypass this check with SQL injection. The following shows a typical database query in Rails to find the first record in the users table which matches the login credentials parameters supplied by the user. ```ruby -User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'") +User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'") ``` If an attacker enters ' OR '1'='1 as the name, and ' OR '2'>'1 as the password, the resulting SQL query will be: @@ -762,7 +768,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title)) This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags. -As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, and > by their uninterpreted representations in HTML (`&`, `"`, `<`, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources. +As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, and > by their uninterpreted representations in HTML (`&`, `"`, `<`, and `>`). ##### Obfuscation and Encoding Injection @@ -797,7 +803,7 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._ -CSS Injection is explained best by the well-known [MySpace Samy worm](http://namb.la/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. +CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this: diff --git a/guides/source/testing.md b/guides/source/testing.md index 98847fde18..e4fc8c7b6e 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -8,7 +8,7 @@ This guide covers built-in mechanisms in Rails for testing your application. After reading this guide, you will know: * Rails testing terminology. -* How to write unit, functional, and integration tests for your application. +* How to write unit, functional, integration, and system tests for your application. * Other popular testing approaches and plugins. -------------------------------------------------------------------------------- @@ -33,18 +33,27 @@ Rails creates a `test` directory for you as soon as you create a Rails project u ```bash $ ls -F test -controllers/ helpers/ mailers/ test_helper.rb -fixtures/ integration/ models/ +controllers/ helpers/ mailers/ system/ test_helper.rb +fixtures/ integration/ models/ application_system_test_case.rb ``` The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers. +The system test directory holds system tests, which are used for full browser +testing of your application. System tests allow you to test your application +the way your users experience it and help you test your JavaScript as well. +System tests inherit from Capybara and perform in browser tests for your +application. + Fixtures are a way of organizing test data; they reside in the `fixtures` directory. A `jobs` directory will also be created when an associated test is first generated. The `test_helper.rb` file holds the default configuration for your tests. +The `application_system_test_case.rb` holds the default configuration for your system +tests. + ### The Test Environment @@ -114,7 +123,7 @@ def test_the_truth end ``` -However only the `test` macro allows a more readable test name. You can still use regular method definitions though. +Although you can still use regular method definitions, using the `test` macro allows for a more readable test name. NOTE: The method name is generated by replacing spaces with underscores. The result does not need to be a valid Ruby identifier though, the name may contain punctuation characters etc. That's because in Ruby technically any string may be a method name. This may require use of `define_method` and `send` calls to function properly, but formally there's little restriction on the name. @@ -322,7 +331,6 @@ specify to make your test failure messages clearer. | `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.| | `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`| | `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`| -| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true, e.g. assert_send [@user, :full_name, 'Sam Smith']. This one is weird eh?| | `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.| The above are a subset of assertions that minitest supports. For an exhaustive & @@ -342,7 +350,9 @@ Rails adds some custom assertions of its own to the `minitest` framework: | --------------------------------------------------------------------------------- | ------- | | [`assert_difference(expressions, difference = 1, message = nil) {...}`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_difference) | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| | [`assert_no_difference(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference) | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| -| [`assert_nothing_raised { block }`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html#method-i-assert_nothing_raised) | Ensures that the given block doesn't raise any exceptions.| +| [`assert_changes(expressions, message = nil, from:, to:, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_changes) | Test that the result of evaluating an expression is changed after invoking the passed in block.| +| [`assert_no_changes(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_changes) | Test the result of evaluating an expression is not changed after invoking the passed in block.| +| [`assert_nothing_raised { block }`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_nothing_raised) | Ensures that the given block doesn't raise any exceptions.| | [`assert_recognizes(expected_options, path, extras={}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes) | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| | [`assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_generates) | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| | [`assert_response(type, message = nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/ResponseAssertions.html#method-i-assert_response) | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.| @@ -359,6 +369,7 @@ All the basic assertions such as `assert_equal` defined in `Minitest::Assertions * [`ActionView::TestCase`](http://api.rubyonrails.org/classes/ActionView/TestCase.html) * [`ActionDispatch::IntegrationTest`](http://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html) * [`ActiveJob::TestCase`](http://api.rubyonrails.org/classes/ActiveJob/TestCase.html) +* [`ActionDispatch::SystemTestCase`](http://api.rubyonrails.org/classes/ActionDispatch/SystemTestCase.html) Each of these classes include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests. @@ -414,7 +425,7 @@ You can also run an entire directory of tests by providing the path to the direc $ bin/rails test test/controllers # run all tests from specific directory ``` -The test runner provides lot of other features too like failing fast, deferring test output +The test runner also provides a lot of other features like failing fast, deferring test output at the end of test run and so on. Check the documentation of the test runner as follows: ```bash @@ -588,6 +599,182 @@ create test/fixtures/articles.yml Model tests don't have their own superclass like `ActionMailer::TestCase` instead they inherit from [`ActiveSupport::TestCase`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html). +System Testing +-------------- + +System tests are full-browser tests that can be used to test your application's +JavaScript and user experience. System tests use Capybara as a base. + +System tests allow for running tests in either a real browser or a headless +driver for testing full user interactions with your application. + +For creating Rails system tests, you use the `test/system` directory in your +application. Rails provides a generator to create a system test skeleton for you. + +```bash +$ bin/rails generate system_test users + invoke test_unit + create test/system/users_test.rb +``` + +Here's what a freshly-generated system test looks like: + +```ruby +require "application_system_test_case" + +class UsersTest < ApplicationSystemTestCase + # test "visiting the index" do + # visit users_url + # + # assert_selector "h1", text: "Users" + # end +end +``` + +By default, system tests are run with the Selenium driver, using the Chrome +browser, and a screen size of 1400x1400. The next section explains how to +change the default settings. + +### Changing the default settings + +Rails makes changing the default settings for system tests very simple. All +the setup is abstracted away so you can focus on writing your tests. + +When you generate a new application or scaffold, an `application_system_test_case.rb` file +is created in the test directory. This is where all the configuration for your +system tests should live. + +If you want to change the default settings you can simply 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 +`application_system_test_case.rb` file do the following: + +```ruby +require "test_helper" +require "capybara/poltergeist" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :poltergeist +end +``` + +The driver name is a required argument for `driven_by`. The optional arguments +that can be passed to `driven_by` are `:using` for the browser (this will only +be used by Selenium), and `:screen_size` to change the size of the screen for +screenshots. + +```ruby +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :firefox +end +``` + +If your Capybara configuration requires more setup than provided by Rails, all +of that configuration can be put into the `application_system_test_case.rb` file. + +Please see [Capybara's documentation](https://github.com/teamcapybara/capybara#setup) +for additional settings. + +### Screenshot Helper + +The `ScreenshotHelper` is a helper designed to capture screenshots of your tests. +This can be helpful for viewing the browser at the point a test failed, or +to view screenshots later for debugging. + +Two methods are provided: `take_screenshot` and `take_failed_screenshot`. +`take_failed_screenshot` is automatically included in `after_teardown` inside +Rails. + +The `take_screenshot` helper method can be included anywhere in your tests to +take a screenshot of the browser. + +### Implementing a system test + +Now we're going to add a system test to our blog application. We'll demonstrate +writing a system test by visiting the index page and creating a new blog article. + +If you used the scaffold generator, a system test skeleton is automatically +created for you. If you did not use the generator start by creating a system +test skeleton. + +```bash +$ bin/rails generate system_test articles +``` + +It should have created a test file placeholder for us. With the output of the +previous command you should see: + +```bash + invoke test_unit + create test/system/articles_test.rb +``` + +Now let's open that file and write our first assertion: + +```ruby +require "application_system_test_case" + +class ArticlesTest < ApplicationSystemTestCase + test "viewing the index" do + visit articles_path + assert_selector "h1", text: "Articles" + end +end +``` + +The test should see that there is an `h1` on the articles index page and pass. + +Run the system tests. + +```bash +bin/rails test:system +``` + +NOTE: By default, running `bin/rails test` won't run your system tests. +Make sure to run `bin/rails test:system` to actually run them. + +#### Creating articles system test + +Now let's test the flow for creating a new article in our blog. + +```ruby +test "creating an article" do + visit articles_path + + click_on "New Article" + + fill_in "Title", with: "Creating an Article" + fill_in "Body", with: "Created this article successfully!" + + click_on "Create Article" + + assert_text "Creating an Article" +end +``` + +The first step is to call `visit articles_path`. This will take the test to the +articles index page. + +Then the `click_on "New Article"` will find the "New Article" button on the +index page. This will redirect the browser to `/articles/new`. + +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 +that the text from the new article's title is on the articles index page. + +#### Taking it further + +The beauty of system testing is that it is similar to integration testing in +that it tests the user's interaction with your controller, model, and view, but +system testing is much more robust and actually tests your application as if +a real user were using it. Going forward, you can test anything that the user +themselves would do in your application such as commenting, deleting articles, +publishing draft articles, etc. Integration Testing ------------------- @@ -800,6 +987,13 @@ end Now you can try running all the tests and they should pass. +NOTE: If you followed the steps in the Basic Authentication section, you'll need to add the following to the `setup` block to get all the tests passing: + +```ruby +request.headers['Authorization'] = ActionController::HttpAuthentication::Basic. + encode_credentials('dhh', 'secret') +``` + ### Available Request Types for Functional Tests If you're familiar with the HTTP protocol, you'll know that `get` is a type of request. There are 6 request types supported in Rails functional tests: @@ -859,10 +1053,10 @@ You also have access to three instance variables in your functional tests, after class ArticlesControllerTest < ActionDispatch::IntegrationTest test "should get index" do get articles_url - + assert_equal "index", @controller.action_name assert_equal "application/x-www-form-urlencoded", @request.media_type - assert_match "Articles", @response.body + assert_match "Articles", @response.body end end ``` @@ -1214,7 +1408,7 @@ In order to test that your mailer is working as expected, you can use unit tests For the purposes of unit testing a mailer, fixtures are used to provide an example of how the output _should_ look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory apart from the other fixtures. The name of the directory within `test/fixtures` directly corresponds to the name of the mailer. So, for a mailer named `UserMailer`, the fixtures should reside in `test/fixtures/user_mailer` directory. -When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator, you'll have to create those files yourself. +If you generated your mailer, the generator does not create stub fixtures for the mailers actions. You'll have to create those files yourself as described above. #### The Basic Test Case @@ -1248,6 +1442,10 @@ variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect. The helper `read_fixture` is used to read in the content from this file. +NOTE: `email.body.to_s` is present when there's only one (HTML or text) part present. +If the mailer provides both, you can test your fixture against specific parts +with `email.text_part.body.to_s` or `email.html_part.body.to_s`. + Here's the content of the `invite` fixture: ``` @@ -1286,7 +1484,7 @@ class UserControllerTest < ActionDispatch::IntegrationTest assert_equal "You have been invited by me@example.com", invite_email.subject assert_equal 'friend@example.com', invite_email.to[0] - assert_match(/Hi friend@example.com/, invite_email.body.to_s) + assert_match(/Hi friend@example\.com/, invite_email.body.to_s) end end ``` @@ -1317,8 +1515,8 @@ end This test is pretty simple and only asserts that the job get the work done as expected. -By default, `ActiveJob::TestCase` will set the queue adapter to `:async` so that -your jobs are performed in an async fashion. It will also ensure that all previously performed +By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that +your jobs are performed inline. It will also ensure that all previously performed and enqueued jobs are cleared before any test run so you can safely assume that no jobs have already been executed in the scope of each test. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 2372590cec..93864db141 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -65,6 +65,41 @@ 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.0 to Rails 5.1 +------------------------------------- + +For more information on changes made to Rails 5.1 please see the [release notes](5_1_release_notes.html). + +### Top-level `HashWithIndifferentAccess` is soft-deprecated + +If your application uses the 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 +moment and no deprecation warning will be displayed, but this constant will be +removed in the future. + +Also, if you have pretty old YAML documents containing dumps of such objects, +you may need to load and dump them again to make sure that they reference +the right constant, and that loading them won't break in the future. + +### `application.secrets` now loaded with all keys as symbols + +If your application stores nested configuration in `config/secrets.yml`, all keys +are now loaded as symbols, so access using strings should be changed. + +From: + +```ruby +Rails.application.secrets[:smtp_settings]["address"] +``` + +To: + +```ruby +Rails.application.secrets[:smtp_settings][:address] +``` + Upgrading from Rails 4.2 to Rails 5.0 ------------------------------------- @@ -140,6 +175,8 @@ See [#19034](https://github.com/rails/rails/pull/19034) for more details. ### Rails Controller Testing +#### Extraction of some helper methods to `rails-controller-testing` + `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. @@ -147,6 +184,14 @@ your Gemfile. If you are using Rspec for testing, please see the extra configuration required in the gem's documentation. +#### New behavior when uploading files + +If you are using `ActionDispatch::Http::UploadedFile` in your tests to +upload files, you will need to change to use the similar `Rack::Test::UploadedFile` +class instead. + +See [#26404](https://github.com/rails/rails/issues/26404) for more details. + ### Autoloading is Disabled After Booting in the Production Environment Autoloading is now disabled after booting in the production environment by @@ -325,7 +370,7 @@ should support caching. #### Configure the Output of `db:structure:dump` -If you're using `schema_search_path` or other PostgreSQL extentions, you can control how the schema is +If you're using `schema_search_path` or other PostgreSQL extensions, you can control how the schema is dumped. Set to `:all` to generate all dumps, or to `:schema_search_path` to generate from schema search path. config.active_record.dump_schemas = :all @@ -703,7 +748,7 @@ There are a few major changes related to JSON handling in Rails 4.1. MultiJSON has reached its [end-of-life](https://github.com/rails/rails/pull/10576) and has been removed from Rails. -If your application currently depend on MultiJSON directly, you have a few options: +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 @@ -1278,6 +1323,10 @@ Also check your environment settings for `config.action_dispatch.best_standards_ Rails 4.0 removes the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`. +#### Cache + +The caching method changed between Rails 3.x and 4.0. You should [change the cache namespace](http://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-store) and roll out with a cold cache. + ### Helpers Loading Order The order in which helpers from more than one directory are loaded has changed in Rails 4.0. Previously, they were gathered and then sorted alphabetically. After upgrading to Rails 4.0, helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use the `helpers_path` parameter, this change will only impact the way of loading helpers from engines. If you rely on the ordering, you should check if correct methods are available after upgrade. If you would like to change the order in which engines are loaded, you can use `config.railties_order=` method. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index c1dfcab6f3..290f2a509b 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -141,6 +141,8 @@ follow this pattern. Built-in Helpers ---------------------- +### Remote elements + Rails provides a bunch of view helper methods written in Ruby to assist you in generating HTML. Sometimes, you want to add a little Ajax to those elements, and Rails has got your back in those cases. @@ -149,18 +151,22 @@ Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two parts: the JavaScript half and the Ruby half. Unless you have disabled the Asset Pipeline, -[rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js) +[rails-ujs](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs.coffee) provides the JavaScript half, and the regular Ruby view helpers add appropriate tags to your DOM. -### form_for +You can read below about the different events that are fired dealing with +remote elements inside your application. + +#### form_with -[`form_for`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for) -is a helper that assists with writing forms. `form_for` takes a `:remote` -option. It works like this: +[`form_with`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) +is a helper that assists with writing forms. By default, `form_with` assumes that +your form will be using Ajax. You can opt out of this behavior by +passing the `:local` option `form_with`. ```erb -<%= form_for(@article, remote: true) do |f| %> +<%= form_with(model: @article) do |f| %> ... <% end %> ``` @@ -168,7 +174,7 @@ option. It works like this: This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post"> +<form action="/articles" method="post" data-remote="true"> ... </form> ``` @@ -189,32 +195,9 @@ $(document).ready -> ``` Obviously, you'll want to be a bit more sophisticated than that, but it's a -start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax). - -### form_tag - -[`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag) -is very similar to `form_for`. It has a `:remote` option that you can use like -this: - -```erb -<%= form_tag('/articles', remote: true) do %> - ... -<% end %> -``` - -This will generate the following HTML: - -```html -<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post"> - ... -</form> -``` - -Everything else is the same as `form_for`. See its documentation for full -details. +start. -### link_to +#### link_to [`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) is a helper that assists with generating links. It has a `:remote` option you @@ -230,7 +213,7 @@ which generates <a href="/articles/1" data-remote="true">an article</a> ``` -You can bind to the same Ajax events as `form_for`. Here's an example. Let's +You can bind to the same Ajax events as `form_with`. Here's an example. Let's assume that we have a list of articles that can be deleted with just one click. We would generate some HTML like this: @@ -246,7 +229,7 @@ $ -> alert "The article was deleted." ``` -### button_to +#### button_to [`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this: @@ -262,7 +245,136 @@ this generates </form> ``` -Since it's just a `<form>`, all of the information on `form_for` also applies. +Since it's just a `<form>`, all of the information on `form_with` also applies. + +### Customize remote elements + +It is possible to customize the behavior of elements with a `data-remote` +attribute without writing a line of JavaScript. You can specify extra `data-` +attributes to accomplish this. + +#### `data-method` + +Activating hyperlinks always results in an HTTP GET request. However, if your +application is [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer), +some links are in fact actions that change data on the server, and must be +performed with non-GET requests. This attribute allows marking up such links +with an explicit method such as "post", "put" or "delete". + +The way it works is that, when the link is activated, it constructs a hidden form +in the document with the "action" attribute corresponding to "href" value of the +link, and the method corresponding to `data-method` value, and submits that form. + +NOTE: Because submitting forms with HTTP methods other than GET and POST isn't +widely supported across browsers, all other HTTP methods are actually sent over +POST with the intended method indicated in the `_method` parameter. Rails +automatically detects and compensates for this. + +#### `data-url` and `data-params` + +Certain elements of your page aren't actually referring to any URL, but you may want +them to trigger Ajax calls. Specifying the `data-url` attribute along with +the `data-remote` one will trigger an Ajax call to the given URL. You can also +specify extra parameters through the `data-params` attribute. + +This can be useful to trigger an action on check-boxes for instance: + +```html +<input type="checkbox" data-remote="true" + data-url="/update" data-params="id=10" data-method="put"> +``` + +#### `data-type` + +It is also possible to define the Ajax `dataType` explicitly while performing +requests for `data-remote` elements, by way of the `data-type` attribute. + +### Confirmations + +You can ask for an extra confirmation of the user by adding a `data-confirm` +attribute on links and forms. The user will be presented a JavaScript `confirm()` +dialog containing the attribute's text. If the user chooses to cancel, the action +doesn't take place. + +Adding this attribute on links will trigger the dialog on click, and adding it +on forms will trigger it on submit. For example: + +```erb +<%= link_to "Dangerous zone", dangerous_zone_path, + data: { confirm: 'Are you sure?' } %> +``` + +This generates: + +```html +<a href="..." data-confirm="Are you sure?">Dangerous zone</a> +``` + +The attribute is also allowed on form submit buttons. This allows you to customize +the warning message depending on the button which was activated. In this case, +you should **not** have `data-confirm` on the form itself. + +The default confirmation uses a JavaScript confirm dialog, but you can customize +this by listening to the `confirm` event, which is fired just before the confirmation +window appears to the user. To cancel this default confirmation, have the confirm +handler to return `false`. + +### Automatic disabling + +It is also possible to automatically disable an input while the form is submitting +by using the `data-disable-with` attribute. This is to prevent accidental +double-clicks from the user, which could result in duplicate HTTP requests that +the backend may not detect as such. The value of the attribute is the text that will +become the new value of the button in its disabled state. + +This also works for links with `data-method` attribute. + +For example: + +```erb +<%= form_with(model: @article.new) do |f| %> + <%= f.submit data: { "disable-with": "Saving..." } %> +<%= end %> +``` + +This generates a form with: + +```html +<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. Server-Side Concerns -------------------- @@ -297,7 +409,7 @@ The index view (`app/views/users/index.html.erb`) contains: <br> -<%= form_for(@user, remote: true) do |f| %> +<%= form_with(model: @user) do |f| %> <%= f.label :name %><br> <%= f.text_field :name %> <%= f.submit %> @@ -338,7 +450,7 @@ this: end ``` -Notice the format.js in the `respond_to` block; that allows the controller to +Notice the `format.js` in the `respond_to` block: that allows the controller to respond to your Ajax request. You then have a corresponding `app/views/users/create.js.erb` view file that generates the actual JavaScript code that will be sent and executed on the client side. @@ -355,7 +467,7 @@ which uses Ajax to speed up page rendering in most applications. ### How Turbolinks Works -Turbolinks attaches a click handler to all `<a>` on the page. If your browser +Turbolinks attaches a click handler to all `<a>` tags on the page. If your browser supports [PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method), Turbolinks will make an Ajax request for the page, parse the response, and @@ -385,7 +497,7 @@ $(document).ready -> ``` However, because Turbolinks overrides the normal page loading process, the -event that this relies on will not be fired. If you have code that looks like +event that this relies upon will not be fired. If you have code that looks like this, you must change your code to do this instead: ```coffeescript diff --git a/guides/w3c_validator.rb b/guides/w3c_validator.rb index 2ce27e2e16..4671e040ca 100644 --- a/guides/w3c_validator.rb +++ b/guides/w3c_validator.rb @@ -32,7 +32,8 @@ include W3CValidators module RailsGuides class Validator def validate - validator = MarkupValidator.new + # https://github.com/w3c-validators/w3c_validators/issues/25 + validator = NuValidator.new STDOUT.sync = true errors_on_guides = {} @@ -44,11 +45,11 @@ module RailsGuides next end - if results.validity - print "." - else + if results.errors.length > 0 print "E" errors_on_guides[f] = results.errors + else + print "." end end @@ -81,7 +82,7 @@ module RailsGuides error_summary += "\n #{name}" error_detail += "\n\n #{name} has #{errors.size} validation error(s):\n" errors.each do |error| - error_detail += "\n "+error.to_s.delete("\n") + error_detail += "\n " + error.to_s.delete("\n") end end diff --git a/rails.gemspec b/rails.gemspec index 2d5be58c17..91316f089f 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index b03c87e9ba..d93c532c7e 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,63 +1,17 @@ -* Run `Minitest.after_run` hooks when running `rails test`. +* Allow irb options to be passed from `rails console` command. - *Michael Grosser* - -* Run `before_configuration` callbacks as soon as application constant - inherits from `Rails::Application`. - - Fixes #19880. + Fixes #28988. *Yuji Yaginuma* -* A generated app should not include Uglifier with `--skip-javascript` option. - - *Ben Pickles* - -* Set session store to cookie store internally and remove the initializer from - the generated app. - - *Prathamesh Sonpatki* - -* Set the server host using the `HOST` environment variable. - - *mahnunchik* - -* Add public API to register new folders for `rake notes`: - - config.annotations.register_directories('spec', 'features') - - *John Meehan* - -* Display name of the class defining the initializer along with the initializer - name in the output of `rails initializers`. - - Before: - disable_dependency_loading - - After: - DemoApp::Application.disable_dependency_loading - - *ta1kt0me* - -* Do not run `bundle install` when generating a new plugin. - - Since bundler 1.12.0, the gemspec is validated so the `bundle install` - command will fail just after the gem is created causing confusion to the - users. This change was a bug fix to correctly validate gemspecs. - - *Rafael Mendonça França* - -* Default `config.assets.quiet = true` in the development environment. Suppress - logging of assets requests by default. - - *Kevin McPhillips* +* Added a shared section to config/database.yml that will be loaded for all environments. -* Ensure `/rails/info` routes match in development for apps with a catch-all globbing route. + *Pierre Schambacher* - *Nicholas Firth-McCoy* +* Namespace error pages' CSS selectors to stop the styles from bleeding into other pages + when using Turbolinks. -* Added a shared section to `config/secrets.yml` that will be loaded for all environments. + *Jan Krutisch* - *DHH* -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/railties/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index 1f496cf280..f9e4444f07 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index ef9bbf3d7e..654c7bae57 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -57,7 +57,7 @@ can read more about Action Pack in its {README}[link:files/actionpack/README_rdo * The \README file created within your application. * {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html]. -* {Ruby on \Rails Tutorial}[http://www.railstutorial.org/book]. +* {Ruby on \Rails Tutorial}[https://www.railstutorial.org/book]. * {Ruby on \Rails Guides}[http://guides.rubyonrails.org]. * {The API Documentation}[http://api.rubyonrails.org]. diff --git a/railties/Rakefile b/railties/Rakefile index 202644fb26..d6284b7dc5 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -16,9 +16,10 @@ namespace :test do dash_i = [ "test", "lib", - "#{File.dirname(__FILE__)}/../activesupport/lib", - "#{File.dirname(__FILE__)}/../actionpack/lib", - "#{File.dirname(__FILE__)}/../activemodel/lib" + "#{__dir__}/../activesupport/lib", + "#{__dir__}/../actionpack/lib", + "#{__dir__}/../actionview/lib", + "#{__dir__}/../activemodel/lib" ] ruby "-w", "-I#{dash_i.join ':'}", file end @@ -26,7 +27,7 @@ namespace :test do end Rake::TestTask.new("test:regular") do |t| - t.libs << "test" << "#{File.dirname(__FILE__)}/../activesupport/lib" + t.libs << "test" << "#{__dir__}/../activesupport/lib" t.pattern = "test/**/*_test.rb" t.warning = false t.verbose = true diff --git a/railties/bin/test b/railties/bin/test new file mode 100755 index 0000000000..a7beb14b27 --- /dev/null +++ b/railties/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require File.expand_path("../tools/test", COMPONENT_ROOT) diff --git a/railties/exe/rails b/railties/exe/rails index 7e791c1f99..a5635c2297 100755 --- a/railties/exe/rails +++ b/railties/exe/rails @@ -1,9 +1,9 @@ #!/usr/bin/env ruby -git_path = File.expand_path("../../../.git", __FILE__) +git_path = File.expand_path("../../.git", __dir__) if File.exist?(git_path) - railties_path = File.expand_path("../../lib", __FILE__) + railties_path = File.expand_path("../lib", __dir__) $:.unshift(railties_path) end require "rails/cli" diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index 5d862e3fec..6d8bf28943 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -7,6 +7,7 @@ require "active_support/dependencies/autoload" require "active_support/core_ext/kernel/reporting" require "active_support/core_ext/module/delegation" require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/object/blank" require "rails/application" require "rails/version" @@ -53,7 +54,7 @@ module Rails end # Returns a Pathname object of the current Rails project, - # otherwise it returns nil if there is no project: + # otherwise it returns +nil+ if there is no project: # # Rails.root # # => #<Pathname:/Users/someuser/some/path/project> @@ -67,7 +68,7 @@ module Rails # Rails.env.development? # => true # Rails.env.production? # => false def env - @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development") + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development") end # Sets the Rails environment. @@ -86,8 +87,8 @@ module Rails # groups assets: [:development, :test] # # # Returns - # # => [:default, :development, :assets] for Rails.env == "development" - # # => [:default, :production] for Rails.env == "production" + # # => [:default, "development", :assets] for Rails.env == "development" + # # => [:default, "production"] for Rails.env == "production" def groups(*groups) hash = groups.extract_options! env = Rails.env @@ -100,7 +101,7 @@ module Rails end # Returns a Pathname object of the public folder of the current - # Rails project, otherwise it returns nil if there is no project: + # Rails project, otherwise it returns +nil+ if there is no project: # # Rails.public_path # # => #<Pathname:/Users/someuser/some/path/project/public> diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb new file mode 100644 index 0000000000..dcc491783c --- /dev/null +++ b/railties/lib/rails/api/generator.rb @@ -0,0 +1,28 @@ +require "sdoc" + +class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc: + RDoc::RDoc.add_generator self + + def generate_class_tree_level(classes, visited = {}) + # Only process core extensions on the first visit. + if visited.empty? + core_exts, classes = classes.partition { |klass| core_extension?(klass) } + + super.unshift([ "Core extensions", "", "", build_core_ext_subtree(core_exts, visited) ]) + else + super + end + end + + private + def build_core_ext_subtree(classes, visited) + classes.map do |klass| + [ klass.name, klass.document_self_or_methods ? klass.path : "", "", + generate_class_tree_level(klass.classes_and_modules, visited) ] + end + end + + def core_extension?(klass) + klass.name != "ActiveSupport" && klass.in_files.any? { |file| file.absolute_name.include?("core_ext") } + end +end diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index bc670b1d75..49267c2329 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -1,4 +1,5 @@ require "rdoc/task" +require_relative "generator" module Rails module API @@ -8,8 +9,7 @@ module Rails include: %w( README.rdoc lib/active_support/**/*.rb - ), - exclude: "lib/active_support/vendor/*" + ) }, "activerecord" => { @@ -69,7 +69,11 @@ module Rails README.rdoc lib/**/*.rb ), - exclude: "lib/rails/generators/rails/**/templates/**/*.rb" + exclude: %w( + lib/rails/generators/**/templates/**/*.rb + lib/rails/test_unit/* + lib/rails/api/generator.rb + ) } } @@ -80,7 +84,7 @@ module Rails # Be lazy computing stuff to have as light impact as possible to # the rest of tasks. before_running_rdoc do - load_and_configure_sdoc + configure_sdoc configure_rdoc_files setup_horo_variables end @@ -91,20 +95,15 @@ module Rails # no-op end - def load_and_configure_sdoc - require "sdoc" - + def configure_sdoc self.title = "Ruby on Rails API" self.rdoc_dir = api_dir options << "-m" << api_main options << "-e" << "UTF-8" - options << "-f" << "sdoc" + options << "-f" << "api" options << "-T" << "rails" - rescue LoadError - $stderr.puts %(Unable to load SDoc, please add\n\n gem 'sdoc', require: false\n\nto the Gemfile.) - exit 1 end def configure_rdoc_files @@ -147,7 +146,7 @@ module Rails end class RepoTask < Task - def load_and_configure_sdoc + def configure_sdoc super options << "-g" # link to GitHub, SDoc flag end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index b01196e3ed..39ca2db8e1 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -4,6 +4,7 @@ require "active_support/core_ext/object/blank" require "active_support/key_generator" require "active_support/message_verifier" require "rails/engine" +require "rails/secrets" module Rails # An Engine with the responsibility of coordinating the whole boot process. @@ -72,7 +73,7 @@ module Rails # on one of the applications to create a copy of the application which shares # the configuration. # - # If you decide to define rake tasks, runners, or initializers in an + # If you decide to define Rake tasks, runners, or initializers in an # application other than +Rails.application+, then you must run them manually. class Application < Engine autoload :Bootstrap, "rails/application/bootstrap" @@ -259,14 +260,15 @@ module Rails "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, + "action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest ) end end - # If you try to define a set of rake tasks on the instance, these will get - # passed up to the rake tasks defined on the application's class. + # If you try to define a set of Rake tasks on the instance, these will get + # passed up to the Rake tasks defined on the application's class. def rake_tasks(&block) self.class.rake_tasks(&block) end @@ -274,7 +276,7 @@ module Rails # Sends the initializers to the +initializer+ method defined in the # Rails::Initializable module. Each Rails::Application class has its own # set of initializers, as defined by the Initializable module. - def initializer(name, opts={}, &block) + def initializer(name, opts = {}, &block) self.class.initializer(name, opts, &block) end @@ -347,7 +349,7 @@ module Rails # Initialize the application passing the given group. By default, the # group is :default - def initialize!(group=:default) #:nodoc: + def initialize!(group = :default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true @@ -385,18 +387,9 @@ module Rails def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - yaml = config.paths["config/secrets"].first - - if File.exist?(yaml) - require "erb" - - all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} - shared_secrets = all_secrets["shared"] - env_secrets = all_secrets[Rails.env] - - secrets.merge!(shared_secrets.symbolize_keys) if shared_secrets - secrets.merge!(env_secrets.symbolize_keys) if env_secrets - end + files = config.paths["config/secrets"].existent + files = files.reject { |path| path.end_with?(".enc") } unless config.read_encrypted_secrets + secrets.merge! Rails::Secrets.parse(files, env: Rails.env) # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base @@ -511,7 +504,7 @@ module Rails def validate_secret_key_config! #:nodoc: if secrets.secret_key_base.blank? - ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " + + ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " \ "Read the upgrade documentation to learn more about this new config option." if secrets.secret_token.blank? diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 11da271501..dc0491035d 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -2,6 +2,7 @@ require "fileutils" require "active_support/notifications" require "active_support/dependencies" require "active_support/descendants_tracker" +require "rails/secrets" module Rails class Application @@ -48,8 +49,8 @@ INFO logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR)) logger.level = ActiveSupport::Logger::WARN logger.warn( - "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " + - "(ie, make it writable for user and group: chmod 0664 #{path}). " + + "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " \ + "(ie, make it writable for user and group: chmod 0664 #{path}). " \ "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." ) logger @@ -77,6 +78,10 @@ INFO initializer :bootstrap_hook, group: :all do |app| ActiveSupport.run_load_hooks(:before_initialize, app) end + + initializer :set_secrets_root, group: :all do + Rails::Secrets.root = root + end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 810750ed35..4ffde6198a 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -3,9 +3,6 @@ require "active_support/file_update_checker" require "rails/engine/configuration" require "rails/source_annotation_extractor" -require "active_support/deprecation" -require "active_support/core_ext/string/strip" # for strip_heredoc - module Rails class Application class Configuration < ::Rails::Engine::Configuration @@ -16,14 +13,14 @@ module Rails :railties_order, :relative_url_root, :secret_key_base, :secret_token, :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading + :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, + :read_encrypted_secrets, :log_level - attr_writer :log_level - attr_reader :encoding, :api_only, :static_cache_control + attr_reader :encoding, :api_only def initialize(*) super - self.encoding = "utf-8" + self.encoding = Encoding::UTF_8 @allow_concurrency = nil @consider_all_requests_local = false @filter_parameters = [] @@ -37,7 +34,7 @@ module Rails @session_store = nil @time_zone = "UTC" @beginning_of_week = :monday - @log_level = nil + @log_level = :debug @generators = app_generators @cache_store = [ :file_store, "#{root}/tmp/cache/" ] @railties_order = [:all] @@ -54,35 +51,50 @@ module Rails @debug_exception_response_format = nil @x = Custom.new @enable_dependency_loading = false + @read_encrypted_secrets = false end - def static_cache_control=(value) - ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `config.static_cache_control` is deprecated and will be removed in Rails 5.1. - Please use - `config.public_file_server.headers = { 'Cache-Control' => '#{value}' }` - instead. - eow + def load_defaults(target_version) + case target_version.to_s + when "5.0" + if respond_to?(:action_controller) + action_controller.per_form_csrf_tokens = true + action_controller.forgery_protection_origin_check = true + end - @static_cache_control = value - end + ActiveSupport.to_time_preserves_timezone = true - def serve_static_files - ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `config.serve_static_files` is deprecated and will be removed in Rails 5.1. - Please use `config.public_file_server.enabled` instead. - eow + if respond_to?(:active_record) + active_record.belongs_to_required_by_default = true + end - @public_file_server.enabled - end + self.ssl_options = { hsts: { subdomains: true } } + + when "5.1" + load_defaults "5.0" + + if respond_to?(:assets) + assets.unknown_asset_fallback = false + end + + if respond_to?(:action_view) + action_view.form_with_generates_remote_forms = true + end - def serve_static_files=(value) - ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `config.serve_static_files` is deprecated and will be removed in Rails 5.1. - Please use `config.public_file_server.enabled = #{value}` instead. - eow + when "5.2" + load_defaults "5.1" - @public_file_server.enabled = value + if respond_to?(:active_record) + active_record.cache_versioning = true + end + + if respond_to?(:action_dispatch) + action_dispatch.use_authenticated_cookie_encryption = true + end + + else + raise "Unknown version #{target_version.to_s.inspect}" + end end def encoding=(value) @@ -112,7 +124,7 @@ module Rails @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" - paths.add "config/secrets", with: "config/secrets.yml" + paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" @@ -133,7 +145,14 @@ module Rails config = if yaml && yaml.exist? require "yaml" require "erb" - YAML.load(ERB.new(yaml.read).result) || {} + loaded_yaml = YAML.load(ERB.new(yaml.read).result) || {} + shared = loaded_yaml.delete("shared") + if shared + loaded_yaml.each do |_k, values| + values.reverse_merge!(shared) + end + end + Hash.new(shared).merge(loaded_yaml) elsif ENV["DATABASE_URL"] # Value from ENV['DATABASE_URL'] is set to default database connection # by Active Record. @@ -151,17 +170,13 @@ module Rails raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace end - def log_level - @log_level ||= (Rails.env.production? ? :info : :debug) - end - def colorize_logging ActiveSupport::LogSubscriber.colorize_logging end def colorize_logging=(val) ActiveSupport::LogSubscriber.colorize_logging = val - self.generators.colorize_logging = val + generators.colorize_logging = val end def session_store(new_session_store = nil, **options) diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 14c0a8cbe4..8fe48feefb 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -19,7 +19,6 @@ module Rails if config.public_file_server.enabled headers = config.public_file_server.headers || {} - headers["Cache-Control".freeze] = config.static_cache_control if config.static_cache_control middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers end @@ -41,12 +40,11 @@ module Rails middleware.use ::Rack::Runtime middleware.use ::Rack::MethodOverride unless config.api_only middleware.use ::ActionDispatch::RequestId + middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies - # Must come after Rack::MethodOverride to properly log overridden methods middleware.use ::Rails::Rack::Logger, config.log_tags middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format - middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies unless config.cache_classes middleware.use ::ActionDispatch::Reloader, app.reloader diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index a855e8fab0..c027d06663 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -124,6 +124,7 @@ module Rails # the hook are taken into account. initializer :set_routes_reloader_hook do |app| reloader = routes_reloader + reloader.eager_load = app.config.eager_load reloader.execute_if_updated reloaders << reloader app.reloader.to_run do diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index cf0a4e128f..e02ef629f2 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -4,11 +4,13 @@ module Rails class Application class RoutesReloader attr_reader :route_sets, :paths - delegate :execute_if_updated, :execute, :updated?, to: :updater + attr_accessor :eager_load + delegate :updated?, to: :updater def initialize @paths = [] @route_sets = [] + @eager_load = false end def reload! @@ -19,6 +21,19 @@ module Rails revert end + def execute + ret = updater.execute + route_sets.each(&:eager_load!) if eager_load + ret + end + + def execute_if_updated + if updated = updater.execute_if_updated + route_sets.each(&:eager_load!) if eager_load + end + updated + end + private def updater diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index f8394b8e0a..f7d112900a 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -1,8 +1,8 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path("../templates", __FILE__) + self.view_paths = File.expand_path("templates", __dir__) layout "application" - protected + private def require_local! unless local_request? diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb index 5c833e12ba..3bd18ebfb5 100644 --- a/railties/lib/rails/backtrace_cleaner.rb +++ b/railties/lib/rails/backtrace_cleaner.rb @@ -16,7 +16,7 @@ module Rails add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests add_gem_filters - add_silencer { |line| line !~ APP_DIRS_PATTERN } + add_silencer { |line| !APP_DIRS_PATTERN.match?(line) } end private diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index b3d88147a5..70dce268f1 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -7,7 +7,8 @@ class CodeStatistics #:nodoc: "Model tests", "Mailer tests", "Job tests", - "Integration tests"] + "Integration tests", + "System tests"] HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" } @@ -106,7 +107,7 @@ class CodeStatistics #:nodoc: code = calculate_code tests = calculate_tests - puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}" + puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f / code)}" puts "" end end diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 6065e78fd1..ee020b58f9 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -15,29 +15,39 @@ module Rails include Behavior + HELP_MAPPINGS = %w(-h -? --help) + class << self def hidden_commands # :nodoc: @hidden_commands ||= [] end def environment # :nodoc: - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development" end # Receives a namespace, arguments and the behavior to invoke the command. - def invoke(namespace, args = [], **config) - namespace = namespace.to_s - namespace = "help" if namespace.blank? || Thor::HELP_MAPPINGS.include?(namespace) - namespace = "version" if %w( -v --version ).include? namespace + def invoke(full_namespace, args = [], **config) + namespace = full_namespace = full_namespace.to_s + + if char = namespace =~ /:(\w+)$/ + command_name, namespace = $1, namespace.slice(0, char) + else + command_name = namespace + end + + command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) + command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) - if command = find_by_namespace(namespace) - command.perform(namespace, args, config) + command = find_by_namespace(namespace, command_name) + if command && command.all_commands[command_name] + command.perform(command_name, args, config) else - find_by_namespace("rake").perform(namespace, args, config) + find_by_namespace("rake").perform(full_namespace, args, config) end end - # Rails finds namespaces similar to thor, it only adds one rule: + # Rails finds namespaces similar to Thor, it only adds one rule: # # Command names must end with "_command.rb". This is required because Rails # looks in load paths and loads the command just before it's going to be used. @@ -50,8 +60,10 @@ module Rails # # Notice that "rails:commands:webrat" could be loaded as well, what # Rails looks for is the first and last parts of the namespace. - def find_by_namespace(name) # :nodoc: - lookups = [ name, "rails:#{name}" ] + def find_by_namespace(namespace, command_name = nil) # :nodoc: + lookups = [ namespace ] + lookups << "#{namespace}:#{command_name}" if command_name + lookups.concat lookups.map { |lookup| "rails:#{lookup}" } lookup(lookups) @@ -82,16 +94,16 @@ module Rails [[ "rails", rails ]] + groups.sort.to_a end - protected - def command_type + private + def command_type # :doc: @command_type ||= "command" end - def lookup_paths + def lookup_paths # :doc: @lookup_paths ||= %w( rails/commands commands ) end - def file_lookup_paths + def file_lookup_paths # :doc: @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ] end end diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb index 31b656ec31..a00e58997c 100644 --- a/railties/lib/rails/command/actions.rb +++ b/railties/lib/rails/command/actions.rb @@ -5,14 +5,19 @@ module Rails # This allows us to run `rails server` from other directories, but still get # the main config.ru and properly set the tmp directory. def set_application_directory! - Dir.chdir(File.expand_path("../../", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) + Dir.chdir(File.expand_path("../..", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) end - if defined?(ENGINE_PATH) - def require_application_and_environment! - require ENGINE_PATH + def require_application_and_environment! + require ENGINE_PATH if defined?(ENGINE_PATH) + + if defined?(APP_PATH) + require APP_PATH + Rails.application.require_environment! end + end + if defined?(ENGINE_PATH) def load_tasks Rake.application.init("rails") Rake.application.load_rakefile @@ -24,11 +29,6 @@ module Rails engine.load_generators end else - def require_application_and_environment! - require APP_PATH - Rails.application.require_environment! - end - def load_tasks Rails.application.load_tasks end diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb index 1efcd69e63..4f074df473 100644 --- a/railties/lib/rails/command/base.rb +++ b/railties/lib/rails/command/base.rb @@ -22,7 +22,7 @@ module Rails # Tries to get the description from a USAGE file one folder above the command # root. - def desc(usage = nil, description = nil) + def desc(usage = nil, description = nil, options = {}) if usage super else @@ -56,13 +56,15 @@ module Rails end def perform(command, args, config) # :nodoc: - command = nil if Thor::HELP_MAPPINGS.include?(args.first) + if Rails::Command::HELP_MAPPINGS.include?(args.first) + command, args = "help", [] + end dispatch(command, args.dup, nil, config) end def printing_commands - namespace.sub(/^rails:/, "") + namespaced_commands end def executable @@ -111,7 +113,7 @@ module Rails # For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb` # would return `rails/test`. def default_command_root - path = File.expand_path(File.join("../commands", command_name), __dir__) + path = File.expand_path(File.join("../commands", command_root_namespace), __dir__) path if File.exist?(path) end @@ -129,6 +131,24 @@ module Rails super end end + + def command_root_namespace + (namespace.split(":") - %w( rails )).first + end + + def namespaced_commands + commands.keys.map do |key| + key == command_root_namespace ? key : "#{command_root_namespace}:#{key}" + end + end + end + + def help + if command_name = self.class.command_name + self.class.command_help(shell, command_name) + else + super + end end end end diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb index ce994746a4..4a92f72f16 100644 --- a/railties/lib/rails/command/behavior.rb +++ b/railties/lib/rails/command/behavior.rb @@ -16,13 +16,13 @@ module Rails @subclasses ||= [] end - protected + private # This code is based directly on the Text gem implementation. # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. # # Returns a value representing the "cost" of transforming str1 into str2. - def levenshtein_distance(str1, str2) + def levenshtein_distance(str1, str2) # :doc: s = str1 t = str2 n = s.length @@ -38,12 +38,12 @@ module Rails str2_codepoint_enumerable = str2.each_codepoint str1.each_codepoint.with_index do |char1, i| - e = i+1 + e = i + 1 str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ - d[j+1] + 1, # insertion + d[j + 1] + 1, # insertion e + 1, # deletion d[j] + cost # substitution ].min @@ -58,7 +58,7 @@ module Rails end # Prints a list of generators. - def print_list(base, namespaces) #:nodoc: + def print_list(base, namespaces) return if namespaces.empty? puts "#{base.camelize}:" @@ -71,7 +71,7 @@ module Rails # Receives namespaces in an array and tries to find matching generators # in the load path. - def lookup(namespaces) #:nodoc: + def lookup(namespaces) paths = namespaces_to_paths(namespaces) paths.each do |raw_path| @@ -91,7 +91,7 @@ module Rails end # This will try to load any command in the load path to show in help. - def lookup! #:nodoc: + def lookup! $LOAD_PATH.each do |base| Dir[File.join(base, *file_lookup_paths)].each do |path| begin @@ -107,7 +107,7 @@ module Rails # Convert namespaces to paths by replacing ":" for "/" and adding # an extra lookup. For example, "rails:model" should be searched # in both: "rails/model/model_generator" and "rails/model_generator". - def namespaces_to_paths(namespaces) #:nodoc: + def namespaces_to_paths(namespaces) paths = [] namespaces.each do |namespace| pieces = namespace.split(":") diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb index 7e3a2b011d..7675d3b3d1 100644 --- a/railties/lib/rails/commands/application/application_command.rb +++ b/railties/lib/rails/commands/application/application_command.rb @@ -13,7 +13,7 @@ module Rails end module Command - class ApplicationCommand < Base + class ApplicationCommand < Base # :nodoc: hide_command! def help diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb index 617066f575..ec58540923 100644 --- a/railties/lib/rails/commands/console/console_command.rb +++ b/railties/lib/rails/commands/console/console_command.rb @@ -64,7 +64,7 @@ module Rails end module Command - class ConsoleCommand < Base + class ConsoleCommand < Base # :nodoc: include EnvironmentArgument class_option :sandbox, aliases: "-s", type: :boolean, default: false, @@ -73,14 +73,26 @@ module Rails class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this console under (test/development/production)." + def initialize(args = [], local_options = {}, config = {}) + console_options = [] + + # For the same behavior as OptionParser, leave only options after "--" in ARGV. + termination = local_options.find_index("--") + if termination + console_options = local_options[termination + 1..-1] + local_options = local_options[0...termination] + end + + ARGV.replace(console_options) + super(args, local_options, config) + end + def perform extract_environment_option_from_argument # RAILS_ENV needs to be set before config/application is required. ENV["RAILS_ENV"] = options[:environment] - ARGV.clear # Clear ARGV so IRB doesn't freak. - require_application_and_environment! Rails::Console.start(Rails.application, options) end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb index d3c80da89b..5bfbe58d97 100644 --- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -1,6 +1,3 @@ -require "erb" -require "yaml" - require "rails/command/environment_argument" module Rails @@ -102,14 +99,14 @@ module Rails Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment end - protected - def configurations + private + def configurations # :doc: require APP_PATH ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Base.configurations end - def find_cmd_and_exec(commands, *args) + def find_cmd_and_exec(commands, *args) # :doc: commands = Array(commands) dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR) @@ -134,7 +131,7 @@ module Rails end module Command - class DbconsoleCommand < Base + class DbconsoleCommand < Base # :nodoc: include EnvironmentArgument class_option :include_password, aliases: "-p", type: :boolean, @@ -143,7 +140,7 @@ module Rails class_option :mode, enum: %w( html list line column ), type: :string, desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." - class_option :header, type: :string + class_option :header, type: :boolean class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this console under (test/development/production)." diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb index 5e6b7f9371..281732a936 100644 --- a/railties/lib/rails/commands/destroy/destroy_command.rb +++ b/railties/lib/rails/commands/destroy/destroy_command.rb @@ -2,9 +2,14 @@ require "rails/generators" module Rails module Command - class DestroyCommand < Base - def help # :nodoc: - Rails::Generators.help self.class.command_name + class DestroyCommand < Base # :nodoc: + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end end def perform(*) @@ -12,9 +17,9 @@ module Rails return help unless generator require_application_and_environment! - Rails.application.load_generators + load_generators - Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails.root + Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails::Command.root end end end diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb index b381ca85b9..9dd7ad1012 100644 --- a/railties/lib/rails/commands/generate/generate_command.rb +++ b/railties/lib/rails/commands/generate/generate_command.rb @@ -2,9 +2,14 @@ require "rails/generators" module Rails module Command - class GenerateCommand < Base - def help # :nodoc: - Rails::Generators.help self.class.command_name + class GenerateCommand < Base # :nodoc: + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end end def perform(*) @@ -14,6 +19,8 @@ module Rails require_application_and_environment! load_generators + ARGV.shift + Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root end end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE index 348f41861f..8eb98319d2 100644 --- a/railties/lib/rails/commands/help/USAGE +++ b/railties/lib/rails/commands/help/USAGE @@ -1,27 +1,16 @@ -Usage: bin/rails COMMAND [args] [options] -<% if engine? %> -The common Rails commands available for engines are: - generate Generate new code (short-cut alias: "g") - destroy Undo code generated with "generate" (short-cut alias: "d") - test Run tests (short-cut alias: "t") - -All commands can be run with -h for more information. - -If you want to run any commands that need to be run in context -of the application, like `bin/rails server` or `bin/rails console`, -you should do it from the application's directory (typically test/dummy). -<% else %> The most common rails commands are: - generate Generate new code (short-cut alias: "g") - console Start the Rails console (short-cut alias: "c") - server Start the Rails server (short-cut alias: "s") - test Run tests (short-cut alias: "t") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + test Run tests except system tests (short-cut alias: "t") + test:system Run system tests + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") +<% unless engine? %> + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" +<% end %> All commands can be run with -h (or --help) for more information. -<% end %> In addition to those commands, there are: diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb index 5bcc4c8eee..90d37217fc 100644 --- a/railties/lib/rails/commands/help/help_command.rb +++ b/railties/lib/rails/commands/help/help_command.rb @@ -1,6 +1,6 @@ module Rails module Command - class HelpCommand < Base + class HelpCommand < Base # :nodoc: hide_command! def help(*) diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb index 13eedfc479..207dd5d995 100644 --- a/railties/lib/rails/commands/new/new_command.rb +++ b/railties/lib/rails/commands/new/new_command.rb @@ -1,8 +1,10 @@ module Rails module Command - class NewCommand < Base - def help - Rails::Command.invoke :application, [ "--help" ] + class NewCommand < Base # :nodoc: + no_commands do + def help + Rails::Command.invoke :application, [ "--help" ] + end end def perform(*) diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb index d6d9fe4400..b40ab006af 100644 --- a/railties/lib/rails/commands/plugin/plugin_command.rb +++ b/railties/lib/rails/commands/plugin/plugin_command.rb @@ -1,6 +1,6 @@ module Rails module Command - class PluginCommand < Base + class PluginCommand < Base # :nodoc: hide_command! def help @@ -11,7 +11,7 @@ module Rails "#{executable} new [options]" end - class_option :rc, type: :boolean, default: File.join("~", ".railsrc"), + class_option :rc, type: :string, default: File.join("~", ".railsrc"), desc: "Initialize the plugin command with previous defaults. Uses .railsrc in your home directory by default." class_option :no_rc, desc: "Skip evaluating .railsrc." diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb index a43c884170..075b1fd23d 100644 --- a/railties/lib/rails/commands/rake/rake_command.rb +++ b/railties/lib/rails/commands/rake/rake_command.rb @@ -1,6 +1,6 @@ module Rails module Command - class RakeCommand < Base + class RakeCommand < Base # :nodoc: extend Rails::Command::Actions namespace "rake" @@ -28,9 +28,7 @@ module Rails return @rake_tasks if defined?(@rake_tasks) - ActiveSupport::Deprecation.silence do - require_application_and_environment! - end + require_application_and_environment! Rake::TaskManager.record_task_metadata = true Rake.application.instance_variable_set(:@name, "rails") diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE index dc47a35ff3..b2a6e8493d 100644 --- a/railties/lib/rails/commands/runner/USAGE +++ b/railties/lib/rails/commands/runner/USAGE @@ -8,7 +8,7 @@ Run the Ruby file located at `path/to/filename.rb` after loading the app: <%= executable %> path/to/filename.rb -<% if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ %> +<% unless Gem.win_platform? %> You can also use the runner command as a shebang line for your executables: #!/usr/bin/env <%= File.expand_path(executable) %> diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb index 8db6da8759..6864a9726b 100644 --- a/railties/lib/rails/commands/runner/runner_command.rb +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -1,20 +1,22 @@ module Rails module Command - class RunnerCommand < Base + class RunnerCommand < Base # :nodoc: class_option :environment, aliases: "-e", type: :string, default: Rails::Command.environment.dup, desc: "The environment for the runner to operate under (test/development/production)" - def help - super - puts self.class.desc + no_commands do + def help + super + puts self.class.desc + end end def self.banner(*) "#{super} [<'Some.ruby(code)'> | <filename.rb>]" end - def perform(code_or_file = nil) + def perform(code_or_file = nil, *command_argv) unless code_or_file help exit 1 @@ -25,6 +27,8 @@ module Rails require_application_and_environment! Rails.application.load_runner + ARGV.replace(command_argv) + if File.exist?(code_or_file) $0 = code_or_file Kernel.load code_or_file diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE new file mode 100644 index 0000000000..96e322fe91 --- /dev/null +++ b/railties/lib/rails/commands/secrets/USAGE @@ -0,0 +1,60 @@ +=== Storing Encrypted Secrets in Source Control + +The Rails `secrets` commands helps encrypting secrets to slim a production +environment's `ENV` hash. It's also useful for atomic deploys: no need to +coordinate key changes to get everything working as the keys are shipped +with the code. + +=== Setup + +Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key` +and `config/secrets.yml.enc` files. + +The latter contains all the keys to be encrypted while the former holds the +encryption key. + +Don't lose the key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +secrets. +Don't commit the key! Add `config/secrets.yml.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to +manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start + + +The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`: + + production: + secret_key_base: so-secret-very-hidden-wow + payment_processing_gateway_key: much-safe-very-gaedwey-wow + +But that's where the similarities between `secrets.yml` and `secrets.yml.enc` +end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and +be encrypted. + +A `shared:` top level key is also supported such that any keys there is merged +into the other environments. + +Additionally, Rails won't read encrypted secrets out of the box even if you have +the key. Add this: + + config.read_encrypted_secrets = true + +to the environment you'd like to read encrypted secrets. `bin/rails secrets:setup` +inserts this into the production environment by default. + +=== Editing Secrets + +After `bin/rails secrets:setup`, run `bin/rails secrets:edit`. + +That command opens a temporary file in `$EDITOR` with the decrypted contents of +`config/secrets.yml.enc` to edit the encrypted secrets. + +When the temporary file is next saved the contents are encrypted and written to +`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets +from leaking. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb new file mode 100644 index 0000000000..651411d444 --- /dev/null +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -0,0 +1,60 @@ +require "active_support" +require "rails/secrets" + +module Rails + module Command + class SecretsCommand < Rails::Command::Base # :nodoc: + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def setup + generator.start + end + + def edit + if ENV["EDITOR"].to_s.empty? + say "No $EDITOR to open decrypted secrets in. Assign one like this:" + say "" + say %(EDITOR="mate --wait" bin/rails secrets:edit) + say "" + say "For editors that fork and exit immediately, it's important to pass a wait flag," + say "otherwise the secrets will be saved immediately with no chance to edit." + + return + end + + require_application_and_environment! + + Rails::Secrets.read_for_editing do |tmp_path| + system("\$EDITOR #{tmp_path}") + end + + say "New secrets encrypted and saved." + rescue Interrupt + say "Aborted changing encrypted secrets: nothing saved." + 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("\$EDITOR #{tmp_path}") + generator.skip_secrets_file { setup } + end + end + + private + def generator + require "rails/generators" + require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + + Rails::Generators::EncryptedSecretsGenerator + end + end + end +end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 4349dfdc71..ebb4ae795a 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -7,51 +7,14 @@ require "rails/dev_caching" module Rails class Server < ::Rack::Server class Options - DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze - def parse!(args) - args, options = args.dup, {} - - option_parser(options).parse! args - - options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" - options[:server] = args.shift - options - end - - def option_parser(options) # :nodoc: - OptionParser.new do |opts| - opts.banner = "Usage: rails server [mongrel, thin etc] [options]" - - opts.separator "" - opts.separator "Options:" - - opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } - opts.on("-b", "--binding=IP", String, - "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v } - opts.on("-c", "--config=file", String, - "Uses a custom rackup configuration.") { |v| options[:config] = v } - opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true } - opts.on("-e", "--environment=name", String, - "Specifies the environment to run this server under (test/development/production).", - "Default: development") { |v| options[:environment] = v } - opts.on("-P", "--pid=pid", String, - "Specifies the PID file.", - "Default: tmp/pids/server.pid") { |v| options[:pid] = v } - opts.on("-C", "--[no-]dev-caching", - "Specifies whether to perform caching in development.", - "true or false") { |v| options[:caching] = v } - - opts.separator "" - - opts.on("-h", "--help", "Shows this help message.") { puts opts; exit } - end + Rails::Command::ServerCommand.new([], args).server_options end end - def initialize(*) - super + def initialize(options = nil) + @default_options = options || {} + super(@default_options) set_environment end @@ -90,15 +53,7 @@ module Rails end def default_options - super.merge( - Port: ENV.fetch("PORT", 3000).to_i, - Host: ENV.fetch("HOST", "localhost").dup, - DoNotReverseLookup: true, - environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, - daemonize: false, - caching: nil, - pid: Options::DEFAULT_PID_PATH, - restart_cmd: restart_command) + super.merge(@default_options) end private @@ -139,15 +94,36 @@ module Rails end module Command - class ServerCommand < Base - def help # :nodoc: - puts Rails::Server::Options.new.option_parser(Hash.new) + class ServerCommand < Base # :nodoc: + DEFAULT_PORT = 3000 + DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze + + class_option :port, aliases: "-p", type: :numeric, + desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port + class_option :binding, aliases: "-b", type: :string, + desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", + banner: :IP + class_option :config, aliases: "-c", type: :string, default: "config.ru", + desc: "Uses a custom rackup configuration.", banner: :file + class_option :daemon, aliases: "-d", type: :boolean, default: false, + desc: "Runs server as a Daemon." + class_option :environment, aliases: "-e", type: :string, + desc: "Specifies the environment to run this server under (development/test/production).", banner: :name + class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH, + desc: "Specifies the PID file." + class_option "dev-caching", aliases: "-C", type: :boolean, default: nil, + desc: "Specifies whether to perform caching in development." + + def initialize(args = [], local_options = {}, config = {}) + @original_options = local_options + super + @server = self.args.shift + @log_stdout = options[:daemon].blank? && (options[:environment] || Rails.env) == "development" end def perform set_application_directory! - - Rails::Server.new.tap do |server| + Rails::Server.new(server_options).tap do |server| # Require application after server sets environment to propagate # the --environment option. require APP_PATH @@ -155,6 +131,94 @@ module Rails server.start end end + + no_commands do + def server_options + { + user_supplied_options: user_supplied_options, + server: @server, + log_stdout: @log_stdout, + Port: port, + Host: host, + DoNotReverseLookup: true, + config: options[:config], + environment: environment, + daemonize: options[:daemon], + pid: pid, + caching: options["dev-caching"], + restart_cmd: restart_command + } + end + end + + private + def user_supplied_options + @user_supplied_options ||= begin + # Convert incoming options array to a hash of flags + # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} + user_flag = {} + @original_options.each do |command| + if command.to_s.start_with?("--") + option = command.split("=")[0] + user_flag[option] = true + elsif command =~ /\A(-.)/ + user_flag[Regexp.last_match[0]] = true + end + end + + # Collect all options that the user has explicitly defined so we can + # differentiate them from defaults + user_supplied_options = [] + self.class.class_options.select do |key, option| + if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"] + name = option.name.to_sym + case name + when :port + name = :Port + when :binding + name = :Host + when :"dev-caching" + name = :caching + when :daemonize + name = :daemon + end + user_supplied_options << name + end + end + user_supplied_options << :Host if ENV["HOST"] + user_supplied_options << :Port if ENV["PORT"] + user_supplied_options.uniq + end + end + + def port + options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i + end + + def host + if options[:binding] + options[:binding] + else + default_host = environment == "development" ? "localhost" : "0.0.0.0" + ENV.fetch("HOST", default_host) + end + end + + def environment + options[:environment] || Rails::Command.environment + end + + def restart_command + "bin/rails server #{@server} #{@original_options.join(" ")}" + end + + def pid + File.expand_path(options[:pid]) + end + + def self.banner(*) + "rails server [puma, thin etc] [options]" + end end end end diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb index 1b2e3af9cc..65e16900ba 100644 --- a/railties/lib/rails/commands/test/test_command.rb +++ b/railties/lib/rails/commands/test/test_command.rb @@ -3,15 +3,17 @@ require "rails/test_unit/minitest_plugin" module Rails module Command - class TestCommand < Base - def help # :nodoc: - perform # Hand over help printing to minitest. + class TestCommand < Base # :nodoc: + no_commands do + def help + perform # Hand over help printing to minitest. + end end def perform(*) $LOAD_PATH << Rails::Command.root.join("test") - Minitest.run_via[:rails] = true + Minitest.run_via = :rails require "active_support/testing/autorun" end diff --git a/railties/lib/rails/commands/version/version_command.rb b/railties/lib/rails/commands/version/version_command.rb index 4f3fbfca1b..ac745594ee 100644 --- a/railties/lib/rails/commands/version/version_command.rb +++ b/railties/lib/rails/commands/version/version_command.rb @@ -1,6 +1,6 @@ module Rails module Command - class VersionCommand < Base + class VersionCommand < Base # :nodoc: def perform Rails::Command.invoke :application, [ "--version" ] end diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index 7dfab969e8..fc7d4909f6 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -91,8 +91,8 @@ module Rails attr_reader :hidden_namespaces def initialize - @aliases = Hash.new { |h,k| h[k] = {} } - @options = Hash.new { |h,k| h[k] = {} } + @aliases = Hash.new { |h, k| h[k] = {} } + @options = Hash.new { |h, k| h[k] = {} } @fallbacks = {} @templates = [] @colorize_logging = true diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb index 541d5e3dad..affadc8e09 100644 --- a/railties/lib/rails/console/app.rb +++ b/railties/lib/rails/console/app.rb @@ -5,7 +5,7 @@ module Rails module ConsoleMethods # reference the global "app" instance, created on demand. To recreate the # instance, pass a non-false value as the parameter. - def app(create=false) + def app(create = false) @app_integration_instance = nil if create @app_integration_instance ||= new_session do |sess| sess.host! "www.example.com" @@ -27,7 +27,7 @@ module Rails end # reloads the environment - def reload!(print=true) + def reload!(print = true) puts "Reloading..." if print Rails.application.reloader.reload! true diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 90e7c04d7c..2732485c5a 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -40,7 +40,7 @@ module Rails # # class MyEngine < Rails::Engine # # Add a load path for this specific Engine - # config.autoload_paths << File.expand_path("../lib/some/path", __FILE__) + # config.autoload_paths << File.expand_path("lib/some/path", __dir__) # # initializer "my_engine.add_middleware" do |app| # app.middleware.use MyEngine::Middleware @@ -109,7 +109,7 @@ module Rails # # == Endpoint # - # An engine can also be a rack application. It can be useful if you have a rack application that + # An engine can also be a Rack application. It can be useful if you have a Rack application that # you would like to wrap with +Engine+ and provide with some of the +Engine+'s features. # # To do that, use the +endpoint+ method: @@ -128,7 +128,7 @@ module Rails # # == Middleware stack # - # As an engine can now be a rack endpoint, it can also have a middleware + # As an engine can now be a Rack endpoint, it can also have a middleware # stack. The usage is exactly the same as in <tt>Application</tt>: # # module MyEngine @@ -380,7 +380,7 @@ module Rails def isolate_namespace(mod) engine_name(generate_railtie_name(mod.name)) - self.routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } + routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } self.isolated = true unless mod.respond_to?(:railtie_namespace) @@ -436,7 +436,7 @@ module Rails # Load console and invoke the registered hooks. # Check <tt>Rails::Railtie.console</tt> for more info. - def load_console(app=self) + def load_console(app = self) require "rails/console/app" require "rails/console/helpers" run_console_blocks(app) @@ -445,14 +445,14 @@ module Rails # Load Rails runner and invoke the registered hooks. # Check <tt>Rails::Railtie.runner</tt> for more info. - def load_runner(app=self) + def load_runner(app = self) run_runner_blocks(app) self end # Load Rake, railties tasks and invoke the registered hooks. # Check <tt>Rails::Railtie.rake_tasks</tt> for more info. - def load_tasks(app=self) + def load_tasks(app = self) require "rake" run_tasks_blocks(app) self @@ -460,7 +460,7 @@ module Rails # Load Rails generators and invoke the registered hooks. # Check <tt>Rails::Railtie.generators</tt> for more info. - def load_generators(app=self) + def load_generators(app = self) require "rails/generators" run_generators_blocks(app) Rails::Generators.configure!(app.config.generators) @@ -499,7 +499,7 @@ module Rails paths["app/helpers"].existent end - # Returns the underlying rack application for this engine. + # Returns the underlying Rack application for this engine. def app @app || @app_build_lock.synchronize { @app ||= begin @@ -549,7 +549,7 @@ module Rails load(seed_file) if seed_file end - # Add configured load paths to ruby load paths and remove duplicates. + # Add configured load paths to Ruby's load path, and remove duplicate entries. initializer :set_load_path, before: :bootstrap_hook do _all_load_paths.reverse_each do |path| $LOAD_PATH.unshift(path) if File.directory?(path) @@ -573,7 +573,7 @@ module Rails end initializer :add_routing_paths do |app| - routing_paths = self.paths["config/routes.rb"].existent + routing_paths = paths["config/routes.rb"].existent if routes? || routing_paths.any? app.routes_reloader.paths.unshift(*routing_paths) @@ -643,23 +643,24 @@ module Rails protected - def load_config_initializer(initializer) - ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do - load(initializer) - end - end - def run_tasks_blocks(*) #:nodoc: super paths["lib/tasks"].existent.sort.each { |ext| load(ext) } end - def has_migrations? #:nodoc: - paths["db/migrate"].existent.any? + private + + def load_config_initializer(initializer) # :doc: + ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do + load(initializer) + end end - def self.find_root_with_flag(flag, root_path, default=nil) #:nodoc: + def has_migrations? + paths["db/migrate"].existent.any? + end + def self.find_root_with_flag(flag, root_path, default = nil) #:nodoc: while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}") parent = File.dirname(root_path) root_path = parent != root_path && parent @@ -671,24 +672,22 @@ module Rails Pathname.new File.realpath root end - def default_middleware_stack #:nodoc: + def default_middleware_stack ActionDispatch::MiddlewareStack.new end - def _all_autoload_once_paths #:nodoc: + def _all_autoload_once_paths config.autoload_once_paths end - def _all_autoload_paths #:nodoc: + def _all_autoload_paths @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq end - def _all_load_paths #:nodoc: + def _all_load_paths @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq end - private - def build_request(env) env.merge!(env_config) req = ActionDispatch::Request.new env diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index a23ae44b0b..b9ef63243a 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -1,12 +1,7 @@ -require "rails/command" +unless defined?(APP_PATH) + if File.exist?(File.expand_path("test/dummy/config/application.rb", ENGINE_ROOT)) + APP_PATH = File.expand_path("test/dummy/config/application", ENGINE_ROOT) + end +end -aliases = { - "g" => "generate", - "d" => "destroy", - "t" => "test" -} - -command = ARGV.shift -command = aliases[command] || command - -Rails::Command.invoke command, ARGV +require "rails/commands" diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 147b904679..0c40173c38 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -7,7 +7,7 @@ module Rails attr_accessor :middleware attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths - def initialize(root=nil) + def initialize(root = nil) super() @root = root @generators = app_generators.dup diff --git a/railties/lib/rails/engine/updater.rb b/railties/lib/rails/engine/updater.rb new file mode 100644 index 0000000000..2ecf994a5c --- /dev/null +++ b/railties/lib/rails/engine/updater.rb @@ -0,0 +1,19 @@ +require "rails/generators" +require "rails/generators/rails/plugin/plugin_generator" + +module Rails + class Engine + class Updater + class << self + def generator + @generator ||= Rails::Generators::PluginGenerator.new ["plugin"], + { engine: true }, destination_root: ENGINE_ROOT + end + + def run(action) + generator.send(action) + end + end + end + end +end diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 9c49e0655a..7bacf2e0ba 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -6,7 +6,7 @@ module Rails module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index dd16b44786..8f15f3a594 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -1,4 +1,4 @@ -activesupport_path = File.expand_path("../../../../activesupport/lib", __FILE__) +activesupport_path = File.expand_path("../../../activesupport/lib", __dir__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require "thor/group" @@ -62,251 +62,255 @@ module Rails stylesheets: true, stylesheet_engine: :css, scaffold_stylesheet: true, - test_framework: false, + system_tests: nil, + test_framework: nil, template_engine: :erb } } - def self.configure!(config) #:nodoc: - api_only! if config.api_only - no_color! unless config.colorize_logging - aliases.deep_merge! config.aliases - options.deep_merge! config.options - fallbacks.merge! config.fallbacks - templates_path.concat config.templates - templates_path.uniq! - hide_namespaces(*config.hidden_namespaces) - end + class << self + def configure!(config) #:nodoc: + api_only! if config.api_only + no_color! unless config.colorize_logging + aliases.deep_merge! config.aliases + options.deep_merge! config.options + fallbacks.merge! config.fallbacks + templates_path.concat config.templates + templates_path.uniq! + hide_namespaces(*config.hidden_namespaces) + end - def self.templates_path #:nodoc: - @templates_path ||= [] - end + def templates_path #:nodoc: + @templates_path ||= [] + end - def self.aliases #:nodoc: - @aliases ||= DEFAULT_ALIASES.dup - end + def aliases #:nodoc: + @aliases ||= DEFAULT_ALIASES.dup + end - def self.options #:nodoc: - @options ||= DEFAULT_OPTIONS.dup - end + def options #:nodoc: + @options ||= DEFAULT_OPTIONS.dup + end - # Hold configured generators fallbacks. If a plugin developer wants a - # generator group to fallback to another group in case of missing generators, - # they can add a fallback. - # - # For example, shoulda is considered a test_framework and is an extension - # of test_unit. However, most part of shoulda generators are similar to - # test_unit ones. - # - # Shoulda then can tell generators to search for test_unit generators when - # some of them are not available by adding a fallback: - # - # Rails::Generators.fallbacks[:shoulda] = :test_unit - def self.fallbacks - @fallbacks ||= {} - end + # Hold configured generators fallbacks. If a plugin developer wants a + # generator group to fallback to another group in case of missing generators, + # they can add a fallback. + # + # For example, shoulda is considered a test_framework and is an extension + # of test_unit. However, most part of shoulda generators are similar to + # test_unit ones. + # + # Shoulda then can tell generators to search for test_unit generators when + # some of them are not available by adding a fallback: + # + # Rails::Generators.fallbacks[:shoulda] = :test_unit + def fallbacks + @fallbacks ||= {} + end - # Configure generators for API only applications. It basically hides - # everything that is usually browser related, such as assets and session - # migration generators, and completely disable helpers and assets - # so generators such as scaffold won't create them. - def self.api_only! - hide_namespaces "assets", "helper", "css", "js" - - options[:rails].merge!( - api: true, - assets: false, - helper: false, - template_engine: nil - ) - - if ARGV.first == "mailer" - options[:rails].merge!(template_engine: :erb) + # Configure generators for API only applications. It basically hides + # everything that is usually browser related, such as assets and session + # migration generators, and completely disable helpers and assets + # so generators such as scaffold won't create them. + def api_only! + hide_namespaces "assets", "helper", "css", "js" + + options[:rails].merge!( + api: true, + assets: false, + helper: false, + template_engine: nil + ) + + if ARGV.first == "mailer" + options[:rails].merge!(template_engine: :erb) + end end - end - # Remove the color from output. - def self.no_color! - Thor::Base.shell = Thor::Shell::Basic - end + # Remove the color from output. + def no_color! + Thor::Base.shell = Thor::Shell::Basic + end - # Returns an array of generator namespaces that are hidden. - # Generator namespaces may be hidden for a variety of reasons. - # Some are aliased such as "rails:migration" and can be - # invoked with the shorter "migration", others are private to other generators - # such as "css:scaffold". - def self.hidden_namespaces - @hidden_namespaces ||= begin - orm = options[:rails][:orm] - test = options[:rails][:test_framework] - template = options[:rails][:template_engine] - css = options[:rails][:stylesheet_engine] - - [ - "rails", - "resource_route", - "#{orm}:migration", - "#{orm}:model", - "#{test}:controller", - "#{test}:helper", - "#{test}:integration", - "#{test}:mailer", - "#{test}:model", - "#{test}:scaffold", - "#{test}:view", - "#{test}:job", - "#{template}:controller", - "#{template}:scaffold", - "#{template}:mailer", - "#{css}:scaffold", - "#{css}:assets", - "css:assets", - "css:scaffold" - ] + # Returns an array of generator namespaces that are hidden. + # Generator namespaces may be hidden for a variety of reasons. + # Some are aliased such as "rails:migration" and can be + # invoked with the shorter "migration", others are private to other generators + # such as "css:scaffold". + def hidden_namespaces + @hidden_namespaces ||= begin + orm = options[:rails][:orm] + test = options[:rails][:test_framework] + template = options[:rails][:template_engine] + css = options[:rails][:stylesheet_engine] + + [ + "rails", + "resource_route", + "#{orm}:migration", + "#{orm}:model", + "#{test}:controller", + "#{test}:helper", + "#{test}:integration", + "#{test}:system", + "#{test}:mailer", + "#{test}:model", + "#{test}:scaffold", + "#{test}:view", + "#{test}:job", + "#{template}:controller", + "#{template}:scaffold", + "#{template}:mailer", + "#{css}:scaffold", + "#{css}:assets", + "css:assets", + "css:scaffold" + ] + end end - end - class << self def hide_namespaces(*namespaces) hidden_namespaces.concat(namespaces) end alias hide_namespace hide_namespaces - end - # Show help message with available generators. - def self.help(command = "generate") - puts "Usage: rails #{command} GENERATOR [args] [options]" - puts - puts "General options:" - puts " -h, [--help] # Print generator's options and usage" - puts " -p, [--pretend] # Run but do not make any changes" - puts " -f, [--force] # Overwrite files that already exist" - puts " -s, [--skip] # Skip files that already exist" - puts " -q, [--quiet] # Suppress status output" - puts - puts "Please choose a generator below." - puts - - print_generators - end - - def self.public_namespaces - lookup! - subclasses.map(&:namespace) - end - - def self.print_generators - sorted_groups.each { |b, n| print_list(b, n) } - end + # Show help message with available generators. + def help(command = "generate") + puts "Usage: rails #{command} GENERATOR [args] [options]" + puts + puts "General options:" + puts " -h, [--help] # Print generator's options and usage" + puts " -p, [--pretend] # Run but do not make any changes" + puts " -f, [--force] # Overwrite files that already exist" + puts " -s, [--skip] # Skip files that already exist" + puts " -q, [--quiet] # Suppress status output" + puts + puts "Please choose a generator below." + puts + + print_generators + end - def self.sorted_groups - namespaces = public_namespaces - namespaces.sort! + def public_namespaces + lookup! + subclasses.map(&:namespace) + end - groups = Hash.new { |h,k| h[k] = [] } - namespaces.each do |namespace| - base = namespace.split(":").first - groups[base] << namespace + def print_generators + sorted_groups.each { |b, n| print_list(b, n) } end - rails = groups.delete("rails") - rails.map! { |n| n.sub(/^rails:/, "") } - rails.delete("app") - rails.delete("plugin") + def sorted_groups + namespaces = public_namespaces + namespaces.sort! - hidden_namespaces.each { |n| groups.delete(n.to_s) } + groups = Hash.new { |h, k| h[k] = [] } + namespaces.each do |namespace| + base = namespace.split(":").first + groups[base] << namespace + end - [[ "rails", rails ]] + groups.sort.to_a - end + rails = groups.delete("rails") + rails.map! { |n| n.sub(/^rails:/, "") } + rails.delete("app") + rails.delete("plugin") + rails.delete("encrypted_secrets") - # Rails finds namespaces similar to thor, it only adds one rule: - # - # Generators names must end with "_generator.rb". This is required because Rails - # looks in load paths and loads the generator just before it's going to be used. - # - # find_by_namespace :webrat, :rails, :integration - # - # Will search for the following generators: - # - # "rails:webrat", "webrat:integration", "webrat" - # - # Notice that "rails:generators:webrat" could be loaded as well, what - # Rails looks for is the first and last parts of the namespace. - def self.find_by_namespace(name, base = nil, context = nil) #:nodoc: - lookups = [] - lookups << "#{base}:#{name}" if base - lookups << "#{name}:#{context}" if context - - unless base || context - unless name.to_s.include?(?:) - lookups << "#{name}:#{name}" - lookups << "rails:#{name}" - end - lookups << "#{name}" + hidden_namespaces.each { |n| groups.delete(n.to_s) } + + [[ "rails", rails ]] + groups.sort.to_a end - lookup(lookups) + # Rails finds namespaces similar to Thor, it only adds one rule: + # + # Generators names must end with "_generator.rb". This is required because Rails + # looks in load paths and loads the generator just before it's going to be used. + # + # find_by_namespace :webrat, :rails, :integration + # + # Will search for the following generators: + # + # "rails:webrat", "webrat:integration", "webrat" + # + # Notice that "rails:generators:webrat" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. + def find_by_namespace(name, base = nil, context = nil) #:nodoc: + lookups = [] + lookups << "#{base}:#{name}" if base + lookups << "#{name}:#{context}" if context + + unless base || context + unless name.to_s.include?(?:) + lookups << "#{name}:#{name}" + lookups << "rails:#{name}" + end + lookups << "#{name}" + end - namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] - lookups.each do |namespace| + lookup(lookups) - klass = namespaces[namespace] - return klass if klass - end + namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] + lookups.each do |namespace| - invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) - end + klass = namespaces[namespace] + return klass if klass + end - # Receives a namespace, arguments and the behavior to invoke the generator. - # It's used as the default entry point for generate, destroy and update - # commands. - def self.invoke(namespace, args=ARGV, config={}) - names = namespace.to_s.split(":") - if klass = find_by_namespace(names.pop, names.any? && names.join(":")) - args << "--help" if args.empty? && klass.arguments.any?(&:required?) - klass.start(args, config) - else - options = sorted_groups.flat_map(&:last) - suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) - msg = "Could not find generator '#{namespace}'. " - msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n" - msg << "Run `rails generate --help` for more options." - puts msg + invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) end - end - protected - def self.print_list(base, namespaces) - namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) } - super + # Receives a namespace, arguments and the behavior to invoke the generator. + # It's used as the default entry point for generate, destroy and update + # commands. + def invoke(namespace, args = ARGV, config = {}) + names = namespace.to_s.split(":") + if klass = find_by_namespace(names.pop, names.any? && names.join(":")) + args << "--help" if args.empty? && klass.arguments.any?(&:required?) + klass.start(args, config) + else + options = sorted_groups.flat_map(&:last) + suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) + msg = "Could not find generator '#{namespace}'. " + msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n" + msg << "Run `rails generate --help` for more options." + puts msg + end end - # Try fallbacks for the given base. - def self.invoke_fallbacks_for(name, base) #:nodoc: - return nil unless base && fallbacks[base.to_sym] - invoked_fallbacks = [] - - Array(fallbacks[base.to_sym]).each do |fallback| - next if invoked_fallbacks.include?(fallback) - invoked_fallbacks << fallback + private - klass = find_by_namespace(name, fallback) - return klass if klass + def print_list(base, namespaces) # :doc: + namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) } + super end - nil - end + # Try fallbacks for the given base. + def invoke_fallbacks_for(name, base) + return nil unless base && fallbacks[base.to_sym] + invoked_fallbacks = [] - def self.command_type - @command_type ||= "generator" - end + Array(fallbacks[base.to_sym]).each do |fallback| + next if invoked_fallbacks.include?(fallback) + invoked_fallbacks << fallback - def self.lookup_paths - @lookup_paths ||= %w( rails/generators generators ) - end + klass = find_by_namespace(name, fallback) + return klass if klass + end - def self.file_lookup_paths - @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ] - end + nil + end + + def command_type # :doc: + @command_type ||= "generator" + end + + def lookup_paths # :doc: + @lookup_paths ||= %w( rails/generators generators ) + end + + def file_lookup_paths # :doc: + @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ] + end + end end end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index ab9dc019e2..0bd0615b7e 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -68,7 +68,7 @@ module Rails # add_source "http://gems.github.com/" do # gem "rspec-rails" # end - def add_source(source, options={}, &block) + def add_source(source, options = {}, &block) log :source, source in_root do @@ -96,7 +96,7 @@ module Rails # environment(nil, env: "development") do # "config.action_controller.asset_host = 'localhost:3000'" # end - def environment(data=nil, options={}) + def environment(data = nil, options = {}) sentinel = /class [a-z_:]+ < Rails::Application/i env_file_sentinel = /Rails\.application\.configure do/ data = yield if !data && block_given? @@ -118,7 +118,7 @@ module Rails # git :init # git add: "this.file that.rb" # git add: "onefile.rb", rm: "badfile.cxx" - def git(commands={}) + def git(commands = {}) if commands.is_a?(Symbol) run "git #{commands}" else @@ -137,7 +137,7 @@ module Rails # end # # vendor("foreign.rb", "# Foreign code is fun") - def vendor(filename, data=nil, &block) + def vendor(filename, data = nil, &block) log :vendor, filename create_file("vendor/#{filename}", data, verbose: false, &block) end @@ -150,7 +150,7 @@ module Rails # end # # lib("foreign.rb", "# Foreign code is fun") - def lib(filename, data=nil, &block) + def lib(filename, data = nil, &block) log :lib, filename create_file("lib/#{filename}", data, verbose: false, &block) end @@ -170,7 +170,7 @@ module Rails # end # # rakefile('seed.rake', 'puts "Planting seeds"') - def rakefile(filename, data=nil, &block) + def rakefile(filename, data = nil, &block) log :rakefile, filename create_file("lib/tasks/#{filename}", data, verbose: false, &block) end @@ -188,7 +188,7 @@ module Rails # end # # initializer("api.rb", "API_KEY = '123456'") - def initializer(filename, data=nil, &block) + def initializer(filename, data = nil, &block) log :initializer, filename create_file("config/initializers/#{filename}", data, verbose: false, &block) end @@ -210,7 +210,7 @@ module Rails # rake("db:migrate") # rake("db:migrate", env: "production") # rake("gems:install", sudo: true) - def rake(command, options={}) + def rake(command, options = {}) execute_command :rake, command, options end @@ -219,7 +219,7 @@ module Rails # rails("db:migrate") # rails("db:migrate", env: "production") # rails("gems:install", sudo: true) - def rails_command(command, options={}) + def rails_command(command, options = {}) execute_command :rails, command, options end @@ -260,32 +260,32 @@ module Rails @after_bundle_callbacks << block end - protected + private # Define log for backwards compatibility. If just one argument is sent, # invoke say, otherwise invoke say_status. Differently from say and # similarly to say_status, this method respects the quiet? option given. - def log(*args) + def log(*args) # :doc: if args.size == 1 say args.first.to_s unless options.quiet? else - args << (self.behavior == :invoke ? :green : :red) + args << (behavior == :invoke ? :green : :red) say_status(*args) end end # Runs the supplied command using either "rake ..." or "rails ..." # based on the executor parameter provided. - def execute_command(executor, command, options={}) + def execute_command(executor, command, options = {}) # :doc: log executor, command env = options[:env] || ENV["RAILS_ENV"] || "development" - sudo = options[:sudo] && RbConfig::CONFIG["host_os"] !~ /mswin|mingw/ ? "sudo " : "" + sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : "" in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", verbose: false) } end # Add an extension to the given name based on the platform. - def extify(name) - if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ + def extify(name) # :doc: + if Gem.win_platform? "#{name}.bat" else name @@ -294,7 +294,7 @@ module Rails # Surround string with single quotes if there is no quotes. # Otherwise fall back to double quotes - def quote(value) + def quote(value) # :doc: return value.inspect unless value.is_a? String if value.include?("'") diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index 587c61fd42..d06609e91e 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -37,9 +37,9 @@ module Rails end alias :exists? :existing_migration - protected + private - def on_conflict_behavior + def on_conflict_behavior # :doc: options = base.options.merge(config) if identical? say_status :identical, :blue, relative_existing_migration @@ -54,13 +54,13 @@ module Rails say_status :skip, :yellow else say_status :conflict, :red - raise Error, "Another migration is already named #{migration_file_name}: " + - "#{existing_migration}. Use --force to replace this migration " + + raise Error, "Another migration is already named #{migration_file_name}: " \ + "#{existing_migration}. Use --force to replace this migration " \ "or --skip to ignore conflicted file." end end - def say_status(status, color, message = relative_destination) + def say_status(status, color, message = relative_destination) # :doc: base.shell.say_status(status, message, color) if config[:verbose] end end diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb index 6183944bb0..2679d06fe4 100644 --- a/railties/lib/rails/generators/active_model.rb +++ b/railties/lib/rails/generators/active_model.rb @@ -39,13 +39,13 @@ module Rails # GET edit # PATCH/PUT update # DELETE destroy - def self.find(klass, params=nil) + def self.find(klass, params = nil) "#{klass}.find(#{params})" end # GET new # POST create - def self.build(klass, params=nil) + def self.build(klass, params = nil) if params "#{klass}.new(#{params})" else @@ -59,7 +59,7 @@ module Rails end # PATCH/PUT update - def update(params=nil) + def update(params = nil) "#{name}.update(#{params})" end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 91342c592c..8429b6c7b8 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -30,15 +30,12 @@ module Rails class_option :database, type: :string, aliases: "-d", default: "sqlite3", desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - class_option :javascript, type: :string, aliases: "-j", default: "jquery", - desc: "Preconfigure for selected JavaScript library" + class_option :skip_yarn, type: :boolean, default: false, + desc: "Don't use Yarn for managing JavaScript dependencies" class_option :skip_gemfile, type: :boolean, default: false, desc: "Don't create a Gemfile" - class_option :skip_bundle, type: :boolean, aliases: "-B", default: false, - desc: "Don't run bundle install" - class_option :skip_git, type: :boolean, aliases: "-G", default: false, desc: "Skip .gitignore file" @@ -67,6 +64,9 @@ module Rails class_option :skip_listen, type: :boolean, default: false, desc: "Don't generate configuration that depends on the listen gem" + class_option :skip_coffee, type: :boolean, default: false, + desc: "Don't use CoffeeScript" + class_option :skip_javascript, type: :boolean, aliases: "-J", default: false, desc: "Skip JavaScript files" @@ -76,6 +76,9 @@ module Rails class_option :skip_test, type: :boolean, aliases: "-T", default: false, desc: "Skip test files" + class_option :skip_system_test, type: :boolean, default: false, + desc: "Skip system test files" + class_option :dev, type: :boolean, default: false, desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" @@ -99,9 +102,9 @@ module Rails convert_database_option_for_jruby end - protected + private - def gemfile_entry(name, *args) + def gemfile_entry(name, *args) # :doc: options = args.extract_options! version = args.first github = options[:github] @@ -117,11 +120,12 @@ module Rails self end - def gemfile_entries + def gemfile_entries # :doc: [rails_gemfile_entry, database_gemfile_entry, webserver_gemfile_entry, assets_gemfile_entry, + webpacker_gemfile_entry, javascript_gemfile_entry, jbuilder_gemfile_entry, psych_gemfile_entry, @@ -129,13 +133,13 @@ module Rails @extra_entries].flatten.find_all(&@gem_filter) end - def add_gem_entry_filter + def add_gem_entry_filter # :doc: @gem_filter = lambda { |next_filter, entry| yield(entry) && next_filter.call(entry) }.curry[@gem_filter] end - def builder + def builder # :doc: @builder ||= begin builder_class = get_builder_class builder_class.include(ActionMethods) @@ -143,24 +147,24 @@ module Rails end end - def build(meth, *args) + def build(meth, *args) # :doc: builder.send(meth, *args) if builder.respond_to?(meth) end - def create_root + def create_root # :doc: valid_const? empty_directory "." FileUtils.cd(destination_root) unless options[:pretend] end - def apply_rails_template + def apply_rails_template # :doc: apply rails_template if rails_template rescue Thor::Error, LoadError, Errno::ENOENT => e raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" end - def set_default_accessors! + def set_default_accessors! # :doc: self.destination_root = File.expand_path(app_path, destination_root) self.rails_template = \ case options[:template] @@ -173,32 +177,32 @@ module Rails end end - def database_gemfile_entry + def database_gemfile_entry # :doc: return [] if options[:skip_active_record] gem_name, gem_version = gem_for_database GemfileEntry.version gem_name, gem_version, "Use #{options[:database]} as the database for Active Record" end - def webserver_gemfile_entry + def webserver_gemfile_entry # :doc: return [] if options[:skip_puma] comment = "Use Puma as the app server" - GemfileEntry.new("puma", "~> 3.0", comment) + GemfileEntry.new("puma", "~> 3.7", comment) end - def include_all_railties? + def include_all_railties? # :doc: options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets, :skip_action_cable).none? end - def comment_if(value) + def comment_if(value) # :doc: options[value] ? "# " : "" end - def keeps? + def keeps? # :doc: !options[:skip_keeps] end - def sqlite3? + def sqlite3? # :doc: !options[:skip_active_record] && options[:database] == "sqlite3" end @@ -236,6 +240,7 @@ module Rails def rails_gemfile_entry dev_edge_common = [ + GemfileEntry.github("arel", "rails/arel"), ] if options.dev? [ @@ -253,14 +258,13 @@ module Rails end def rails_version_specifier(gem_version = Rails.gem_version) - if gem_version.prerelease? - next_series = gem_version - next_series = next_series.bump while next_series.segments.size > 2 - - [">= #{gem_version}", "< #{next_series}"] - elsif gem_version.segments.size == 3 + if gem_version.segments.size == 3 || gem_version.release.segments.size == 3 + # ~> 1.2.3 + # ~> 1.2.3.pre4 "~> #{gem_version}" else + # ~> 1.2.3, >= 1.2.3.4 + # ~> 1.2.3, >= 1.2.3.4.pre5 patch = gem_version.segments[0, 3].join(".") ["~> #{patch}", ">= #{gem_version}"] end @@ -271,7 +275,7 @@ module Rails case options[:database] when "mysql" then ["mysql2", [">= 0.3.18", "< 0.5"]] when "postgresql" then ["pg", ["~> 0.18"]] - when "oracle" then ["ruby-oci8", nil] + when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] when "frontbase" then ["ruby-frontbase", nil] when "sqlserver" then ["activerecord-sqlserver-adapter", nil] when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil] @@ -287,7 +291,6 @@ module Rails case options[:database] when "postgresql" then options[:database].replace "jdbcpostgresql" when "mysql" then options[:database].replace "jdbcmysql" - when "oracle" then options[:database].replace "jdbc" when "sqlite3" then options[:database].replace "jdbcsqlite3" end end @@ -297,7 +300,7 @@ module Rails return [] if options[:skip_sprockets] gems = [] - gems << GemfileEntry.github("sass-rails", "rails/sass-rails", nil, + gems << GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets") if !options[:skip_javascript] @@ -309,6 +312,13 @@ module Rails gems end + def webpacker_gemfile_entry + return [] unless options[:webpack] + + comment = "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" + GemfileEntry.new "webpacker", nil, comment + end + def jbuilder_gemfile_entry comment = "Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder" GemfileEntry.new "jbuilder", "~> 2.5", comment, {}, options[:api] @@ -322,9 +332,8 @@ module Rails if options[:skip_javascript] || options[:skip_sprockets] [] else - gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry] - gems << GemfileEntry.version("#{options[:javascript]}-rails", nil, - "Use #{options[:javascript]} as the JavaScript library") + gems = [javascript_runtime_gemfile_entry] + gems << coffee_gemfile_entry unless options[:skip_coffee] unless options[:skip_turbolinks] gems << GemfileEntry.version("turbolinks", "~> 5", @@ -340,7 +349,7 @@ module Rails if defined?(JRUBY_VERSION) GemfileEntry.version "therubyrhino", nil, comment else - GemfileEntry.new "therubyracer", nil, comment, { platforms: :ruby }, true + GemfileEntry.new "mini_racer", nil, comment, { platforms: :ruby }, true end end @@ -392,6 +401,10 @@ module Rails !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end + def depends_on_system_test? + !(options[:skip_system_test] || options[:skip_test] || options[:api]) + end + def depend_on_listen? !options[:skip_listen] && os_supports_listen_out_of_the_box? end @@ -404,6 +417,13 @@ module Rails bundle_command("install") if bundle_install? end + def run_webpack + if !(webpack = options[:webpack]).nil? + rails_command "webpacker:install" + rails_command "webpacker:install:#{webpack}" unless webpack == "webpack" + end + end + def generate_spring_binstubs if bundle_install? && spring_install? bundle_command("exec spring binstub --all") diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 1a0420c769..e7f51dba99 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -20,14 +20,14 @@ module Rails strict_args_position! # Returns the source root for this generator using default_source_root as default. - def self.source_root(path=nil) + def self.source_root(path = nil) @_source_root = path if path @_source_root ||= default_source_root end # Tries to get the description from a USAGE file one folder above the source # root otherwise uses a default description. - def self.desc(description=nil) + def self.desc(description = nil) return super if description @desc ||= if usage_path @@ -40,7 +40,7 @@ module Rails # Convenience method to get the namespace from the class name. It's the # same as Thor default except that the Generator at the end of the class # is removed. - def self.namespace(name=nil) + def self.namespace(name = nil) return super if name @namespace ||= super.sub(/_generator$/, "").sub(/:generators:/, ":") end @@ -195,7 +195,7 @@ module Rails end # Make class option aware of Rails::Generators.options and Rails::Generators.aliases. - def self.class_option(name, options={}) #:nodoc: + def self.class_option(name, options = {}) #:nodoc: options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc) options[:aliases] = default_aliases_for_option(name, options) options[:default] = default_value_for_option(name, options) @@ -215,7 +215,7 @@ module Rails # Returns the base root for a common set of generators. This is used to dynamically # guess the default source root. def self.base_root - File.dirname(__FILE__) + __dir__ end # Cache source root and add lib/generators/base/generator/templates to @@ -239,11 +239,11 @@ module Rails end end - protected + private # Check whether the given class names are already taken by user # application or Ruby on Rails. - def class_collisions(*class_names) #:nodoc: + def class_collisions(*class_names) return unless behavior == :invoke class_names.flatten.each do |class_name| @@ -256,15 +256,15 @@ module Rails last = extract_last_module(nesting) if last && last.const_defined?(last_name.camelize, false) - raise Error, "The name '#{class_name}' is either already used in your application " << - "or reserved by Ruby on Rails. Please choose an alternative and run " << + raise Error, "The name '#{class_name}' is either already used in your application " \ + "or reserved by Ruby on Rails. Please choose an alternative and run " \ "this generator again." end end end # Takes in an array of nested modules and extracts the last module - def extract_last_module(nesting) + def extract_last_module(nesting) # :doc: nesting.inject(Object) do |last_module, nest| break unless last_module.const_defined?(nest, false) last_module.const_get(nest) @@ -272,12 +272,12 @@ module Rails end # Use Rails default banner. - def self.banner - "rails generate #{namespace.sub(/^rails:/,'')} #{arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, " ") + def self.banner # :doc: + "rails generate #{namespace.sub(/^rails:/, '')} #{arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, " ") end # Sets the base_name taking into account the current class namespace. - def self.base_name + def self.base_name # :doc: @base_name ||= begin if base = name.to_s.split("::").first base.underscore @@ -287,7 +287,7 @@ module Rails # Removes the namespaces and get the generator name. For example, # Rails::Generators::ModelGenerator will return "model" as generator name. - def self.generator_name + def self.generator_name # :doc: @generator_name ||= begin if generator = name.to_s.split("::").last generator.sub!(/Generator$/, "") @@ -298,18 +298,18 @@ module Rails # Returns the default value for the option name given doing a lookup in # Rails::Generators.options. - def self.default_value_for_option(name, options) + def self.default_value_for_option(name, options) # :doc: default_for_option(Rails::Generators.options, name, options, options[:default]) end # Returns default aliases for the option name given doing a lookup in # Rails::Generators.aliases. - def self.default_aliases_for_option(name, options) + def self.default_aliases_for_option(name, options) # :doc: default_for_option(Rails::Generators.aliases, name, options, options[:aliases]) end # Returns default for the option name given doing a lookup in config. - def self.default_for_option(config, name, options, default) + def self.default_for_option(config, name, options, default) # :doc: if generator_name && (c = config[generator_name.to_sym]) && c.key?(name) c[name] elsif base_name && (c = config[base_name.to_sym]) && c.key?(name) @@ -331,7 +331,7 @@ module Rails def self.prepare_for_invocation(name, value) #:nodoc: return super unless value.is_a?(String) || value.is_a?(Symbol) - if value && constants = self.hooks[name] + if value && constants = hooks[name] value = name if TrueClass === value Rails::Generators.find_by_namespace(value, *constants) elsif klass = Rails::Generators.find_by_namespace(value) @@ -343,7 +343,7 @@ module Rails # Small macro to add ruby as an option to the generator with proper # default value plus an instance helper method called shebang. - def self.add_shebang_option! + def self.add_shebang_option! # :doc: class_option :ruby, type: :string, aliases: "-r", default: Thor::Util.ruby_command, desc: "Path to the Ruby binary of your choice", banner: "PATH" @@ -361,7 +361,7 @@ module Rails } end - def self.usage_path + def self.usage_path # :doc: paths = [ source_root && File.expand_path("../USAGE", source_root), default_generator_root && File.join(default_generator_root, "USAGE") @@ -369,7 +369,7 @@ module Rails paths.compact.detect { |path| File.exist? path } end - def self.default_generator_root + def self.default_generator_root # :doc: path = File.expand_path(File.join(base_name, generator_name), base_root) path if File.exist?(path) end diff --git a/railties/lib/rails/generators/css/assets/assets_generator.rb b/railties/lib/rails/generators/css/assets/assets_generator.rb index 20baf31a34..af7b5cf609 100644 --- a/railties/lib/rails/generators/css/assets/assets_generator.rb +++ b/railties/lib/rails/generators/css/assets/assets_generator.rb @@ -3,7 +3,7 @@ require "rails/generators/named_base" module Css # :nodoc: module Generators # :nodoc: class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_stylesheet copy_file "stylesheet.css", File.join("app/assets/stylesheets", class_path, "#{file_name}.css") diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb index d01502002f..97d9ab29d4 100644 --- a/railties/lib/rails/generators/erb.rb +++ b/railties/lib/rails/generators/erb.rb @@ -3,7 +3,7 @@ require "rails/generators/named_base" module Erb # :nodoc: module Generators # :nodoc: class Base < Rails::Generators::NamedBase #:nodoc: - protected + private def formats [format] @@ -17,8 +17,8 @@ module Erb # :nodoc: :erb end - def filename_with_extensions(name, format = self.format) - [name, format, handler].compact.join(".") + def filename_with_extensions(name, file_format = format) + [name, file_format, handler].compact.join(".") end end end diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index f150240908..3f1d9932f6 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -9,10 +9,10 @@ module Erb # :nodoc: view_base_path = File.join("app/views", class_path, file_name + "_mailer") empty_directory view_base_path - if self.behavior == :invoke + if behavior == :invoke formats.each do |format| layout_path = File.join("app/views/layouts", class_path, filename_with_extensions("mailer", format)) - template filename_with_extensions(:layout, format), layout_path + template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path) end end @@ -26,7 +26,7 @@ module Erb # :nodoc: end end - protected + private def formats [:text, :html] diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb index 154d85f381..0d77ef21da 100644 --- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -21,7 +21,7 @@ module Erb # :nodoc: end end - protected + private def available_views %w(index edit show new _form) diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 519b6c8603..4f2e84f924 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -1,4 +1,4 @@ -<%%= form_for(<%= singular_table_name %>) do |f| %> +<%%= form_with(model: <%= singular_table_name %>, local: true) do |form| %> <%% if <%= singular_table_name %>.errors.any? %> <div id="error_explanation"> <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> @@ -14,21 +14,21 @@ <% attributes.each do |attribute| -%> <div class="field"> <% if attribute.password_digest? -%> - <%%= f.label :password %> - <%%= f.password_field :password %> + <%%= form.label :password %> + <%%= form.password_field :password, id: :<%= field_id(:password) %> %> </div> <div class="field"> - <%%= f.label :password_confirmation %> - <%%= f.password_field :password_confirmation %> + <%%= form.label :password_confirmation %> + <%%= form.password_field :password_confirmation, id: :<%= field_id(:password_confirmation) %> %> <% else -%> - <%%= f.label :<%= attribute.column_name %> %> - <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> + <%%= form.label :<%= attribute.column_name %> %> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, id: :<%= field_id(attribute.column_name) %> %> <% end -%> </div> <% end -%> <div class="actions"> - <%%= f.submit %> + <%%= form.submit %> </div> <%% end %> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 61181b7b97..baed7bf1e3 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -56,7 +56,7 @@ module Rails end end - def initialize(name, type=nil, index_type=false, attr_options={}) + def initialize(name, type = nil, index_type = false, attr_options = {}) @name = name @type = type || :string @has_index = INDEX_OPTIONS.include?(index_type) @@ -151,7 +151,7 @@ module Rails end def inject_options - "".tap { |s| options_for_migration.each { |k,v| s << ", #{k}: #{v.inspect}" } } + "".tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } } end def inject_index_options diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb index 64d706ec91..52a71b58cd 100644 --- a/railties/lib/rails/generators/js/assets/assets_generator.rb +++ b/railties/lib/rails/generators/js/assets/assets_generator.rb @@ -3,7 +3,7 @@ require "rails/generators/named_base" module Js # :nodoc: module Generators # :nodoc: class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_javascript copy_file "javascript.js", File.join("app/assets/javascripts", class_path, "#{file_name}.js") diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 7290e235a1..82481169c3 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -35,7 +35,7 @@ module Rails end def set_migration_assigns!(destination) - destination = File.expand_path(destination, self.destination_root) + destination = File.expand_path(destination, destination_root) migration_dir = File.dirname(destination) @migration_number = self.class.next_migration_number(migration_dir) @@ -52,7 +52,7 @@ module Rails # # migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb" def migration_template(source, destination, config = {}) - source = File.expand_path(find_in_source_paths(source.to_s)) + source = File.expand_path(find_in_source_paths(source.to_s)) set_migration_assigns!(destination) context = instance_eval("binding") diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index c39ea24935..aef66adb64 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -32,145 +32,153 @@ module Rails end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :file_name + private + # FIXME: We are avoiding to use alias because a bug on thor that make # this method public and add it to the task list. - def singular_name + def singular_name # :doc: file_name end # Wrap block with namespace of current application # if namespace exists and is not skipped - def module_namespacing(&block) + def module_namespacing(&block) # :doc: content = capture(&block) content = wrap_with_namespace(content) if namespaced? concat(content) end - def indent(content, multiplier = 2) + def indent(content, multiplier = 2) # :doc: spaces = " " * multiplier content.each_line.map { |line| line.blank? ? line : "#{spaces}#{line}" }.join end - def wrap_with_namespace(content) + def wrap_with_namespace(content) # :doc: content = indent(content).chomp "module #{namespace.name}\n#{content}\nend\n" end - def inside_template + def inside_template # :doc: @inside_template = true yield ensure @inside_template = false end - def inside_template? + def inside_template? # :doc: @inside_template end - def namespace + def namespace # :doc: Rails::Generators.namespace end - def namespaced? + def namespaced? # :doc: !options[:skip_namespace] && namespace end - def file_path + def namespace_dirs + @namespace_dirs ||= namespace.name.split("::").map(&:underscore) + end + + def file_path # :doc: @file_path ||= (class_path + [file_name]).join("/") end - def class_path + def class_path # :doc: inside_template? || !namespaced? ? regular_class_path : namespaced_class_path end - def regular_class_path + def regular_class_path # :doc: @class_path end - def namespaced_file_path - @namespaced_file_path ||= namespaced_class_path.join("/") + def namespaced_class_path # :doc: + @namespaced_class_path ||= namespace_dirs + @class_path end - def namespaced_class_path - @namespaced_class_path ||= [namespaced_path] + @class_path + def namespaced_path # :doc: + @namespaced_path ||= namespace_dirs.join("/") end - def namespaced_path - @namespaced_path ||= namespace.name.split("::").first.underscore - end - - def class_name + def class_name # :doc: (class_path + [file_name]).map!(&:camelize).join("::") end - def human_name + def human_name # :doc: @human_name ||= singular_name.humanize end - def plural_name + def plural_name # :doc: @plural_name ||= singular_name.pluralize end - def i18n_scope + def i18n_scope # :doc: @i18n_scope ||= file_path.tr("/", ".") end - def table_name + def table_name # :doc: @table_name ||= begin base = pluralize_table_names? ? plural_name : singular_name (class_path + [base]).join("_") end end - def uncountable? + def uncountable? # :doc: singular_name == plural_name end - def index_helper + def index_helper # :doc: uncountable? ? "#{plural_table_name}_index" : plural_table_name end - def show_helper + def show_helper # :doc: "#{singular_table_name}_url(@#{singular_table_name})" end - def edit_helper + def edit_helper # :doc: "edit_#{show_helper}" end - def new_helper + def new_helper # :doc: "new_#{singular_table_name}_url" end - def singular_table_name + 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 - def plural_table_name + def plural_table_name # :doc: @plural_table_name ||= (pluralize_table_names? ? table_name : table_name.pluralize) end - def plural_file_name + def plural_file_name # :doc: @plural_file_name ||= file_name.pluralize end - def fixture_file_name + def fixture_file_name # :doc: @fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name) end - def route_url + def route_url # :doc: @route_url ||= class_path.collect { |dname| "/" + dname }.join + "/" + plural_file_name end - def url_helper_prefix + def url_helper_prefix # :doc: @url_helper_prefix ||= (class_path + [file_name]).join("_") end # Tries to retrieve the application name or simply return application. - def application_name + def application_name # :doc: if defined?(Rails) && Rails.application Rails.application.class.name.split("::").first.underscore else @@ -178,20 +186,20 @@ module Rails end end - def assign_names!(name) #:nodoc: + def assign_names!(name) @class_path = name.include?("/") ? name.split("/") : name.split("::") @class_path.map!(&:underscore) @file_name = @class_path.pop end # Convert attributes array into GeneratedAttribute objects. - def parse_attributes! #:nodoc: + def parse_attributes! self.attributes = (attributes || []).map do |attr| Rails::Generators::GeneratedAttribute.parse(attr) end end - def attributes_names + def attributes_names # :doc: @attributes_names ||= attributes.each_with_object([]) do |a, names| names << a.column_name names << "password_confirmation" if a.password_digest? @@ -199,11 +207,11 @@ module Rails end end - def pluralize_table_names? + def pluralize_table_names? # :doc: !defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names end - def mountable_engine? + def mountable_engine? # :doc: defined?(ENGINE_ROOT) && namespaced? end @@ -217,9 +225,9 @@ module Rails # If the generator is invoked with class name Admin, it will check for # the presence of "AdminDecorator". # - def self.check_class_collision(options={}) + def self.check_class_collision(options = {}) # :doc: define_method :check_class_collision do - name = if self.respond_to?(:controller_class_name) # for ScaffoldBase + name = if respond_to?(:controller_class_name) # for ScaffoldBase controller_class_name else class_name diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 9f9c50ca10..45b9e7bdff 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -32,6 +32,14 @@ module Rails # This allows you to override entire operations, like the creation of the # Gemfile, README, or JavaScript files, without needing to know exactly # what those operations do so you can create another template action. + # + # class CustomAppBuilder < Rails::AppBuilder + # def test + # @generator.gem "rspec-rails", group: [:development, :test] + # run "bundle install" + # generate "rspec:install" + # end + # end class AppBuilder def rakefile template "Rakefile" @@ -53,10 +61,20 @@ module Rails template "gitignore", ".gitignore" end + def version_control + if !options[:skip_git] && !options[:pretend] + run "git init" + end + end + + def package_json + template "package.json" + end + def app directory "app" - keep_file "app/assets/images" + keep_file "app/assets/images" empty_directory_with_keep_file "app/assets/javascripts/channels" unless options[:skip_action_cable] keep_file "app/controllers/concerns" @@ -70,6 +88,16 @@ module Rails chmod "bin", 0755 & ~File.umask, verbose: false end + def bin_when_updating + bin_yarn_exist = File.exist?("bin/yarn") + + bin + + if options[:api] && !bin_yarn_exist + remove_file "bin/yarn" + end + end + def config empty_directory "config" @@ -92,11 +120,10 @@ module Rails cookie_serializer_config_exist = File.exist?("config/initializers/cookies_serializer.rb") action_cable_config_exist = File.exist?("config/cable.yml") rack_cors_config_exist = File.exist?("config/initializers/cors.rb") + assets_config_exist = File.exist?("config/initializers/assets.rb") config - gsub_file "config/environments/development.rb", /^(\s+)config\.file_watcher/, '\1# config.file_watcher' - unless cookie_serializer_config_exist gsub_file "config/initializers/cookies_serializer.rb", /json(?!,)/, "marshal" end @@ -108,6 +135,16 @@ module Rails unless rack_cors_config_exist remove_file "config/initializers/cors.rb" end + + if options[:api] + unless cookie_serializer_config_exist + remove_file "config/initializers/cookies_serializer.rb" + end + + unless assets_config_exist + remove_file "config/initializers/assets.rb" + end + end end def database_yml @@ -144,6 +181,12 @@ module Rails template "test/test_helper.rb" end + def system_test + empty_directory_with_keep_file "test/system" + + template "test/application_system_test_case.rb" + end + def tmp empty_directory_with_keep_file "tmp" empty_directory "tmp/cache" @@ -151,28 +194,19 @@ module Rails end def vendor - vendor_javascripts - vendor_stylesheets - end - - def vendor_javascripts - unless options[:skip_javascript] - empty_directory_with_keep_file "vendor/assets/javascripts" - end - end - - def vendor_stylesheets - empty_directory_with_keep_file "vendor/assets/stylesheets" + empty_directory_with_keep_file "vendor" end end module Generators # We need to store the RAILS_DEV_PATH in a constant, otherwise the path # can change in Ruby 1.8.7 when we FileUtils.cd. - RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__)) + RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__) RESERVED_NAMES = %w[application destroy plugin runner test] class AppGenerator < AppBase # :nodoc: + WEBPACKS = %w( react vue angular elm ) + add_shared_options_for "application" # Add bin/rails options @@ -182,20 +216,24 @@ module Rails class_option :api, type: :boolean, desc: "Preconfigure smaller stack for API only apps" + class_option :skip_bundle, type: :boolean, aliases: "-B", default: false, + desc: "Don't run bundle install" + + class_option :webpack, type: :string, default: nil, + desc: "Preconfigure for app-like JavaScript with Webpack (options: #{WEBPACKS.join('/')})" + def initialize(*args) super - unless app_path - raise Error, "Application name should be provided in arguments. For details run: rails --help" - end - if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end - # Force sprockets to be skipped when generating API only apps. + # Force sprockets and yarn to be skipped when generating API only apps. # Can't modify options hash as it's frozen by default. - self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze if options[:api] + if options[:api] + self.options = options.merge(skip_sprockets: true, skip_javascript: true, skip_yarn: true).freeze + end end public_task :set_default_accessors! @@ -205,8 +243,10 @@ module Rails build(:readme) build(:rakefile) build(:configru) - build(:gitignore) unless options[:skip_git] - build(:gemfile) unless options[:skip_gemfile] + build(:gitignore) unless options[:skip_git] + build(:gemfile) unless options[:skip_gemfile] + build(:version_control) + build(:package_json) unless options[:skip_yarn] end def create_app_files @@ -217,6 +257,11 @@ module Rails build(:bin) end + def update_bin_files + build(:bin_when_updating) + end + remove_task :update_bin_files + def create_config_files build(:config) end @@ -241,6 +286,7 @@ module Rails end def create_db_files + return if options[:skip_active_record] build(:db) end @@ -260,6 +306,10 @@ module Rails build(:test) unless options[:skip_test] end + def create_system_test_files + build(:system_test) if depends_on_system_test? + end + def create_tmp_files build(:tmp) end @@ -273,7 +323,6 @@ module Rails remove_dir "app/assets" remove_dir "lib/assets" remove_dir "tmp/cache/assets" - remove_dir "vendor/assets" end end @@ -321,7 +370,6 @@ module Rails def delete_action_mailer_files_skipping_action_mailer if options[:skip_action_mailer] - remove_file "app/mailers/application_mailer.rb" remove_file "app/views/layouts/mailer.html.erb" remove_file "app/views/layouts/mailer.text.erb" remove_dir "app/mailers" @@ -349,23 +397,33 @@ module Rails end end + def delete_new_framework_defaults + unless options[:update] + remove_file "config/initializers/new_framework_defaults_5_2.rb" + end + end + + def delete_bin_yarn_if_skip_yarn_option + remove_file "bin/yarn" if options[:skip_yarn] + end + def finish_template build(:leftovers) end public_task :apply_rails_template, :run_bundle - public_task :generate_spring_binstubs + public_task :run_webpack, :generate_spring_binstubs def run_after_bundle_callbacks @after_bundle_callbacks.each(&:call) end - protected - def self.banner "rails new #{arguments.map(&:usage).join(' ')} [options]" end + private + # Define file as an alias to create_file for backwards compatibility. def file(*args, &block) create_file(*args, &block) @@ -422,7 +480,7 @@ module Rails "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 "/opt/lampp/var/mysql/mysql.sock" # xampp for linux - ].find { |f| File.exist?(f) } unless RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ + ].find { |f| File.exist?(f) } unless Gem.win_platform? end def get_builder_class diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 7af5fcd3c1..747d2e6253 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } <% gemfile_entries.each do |gem| -%> <% if gem.comment -%> @@ -26,7 +27,12 @@ source 'https://rubygems.org' <% if RUBY_ENGINE == 'ruby' -%> group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug', platform: :mri + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + <%- if depends_on_system_test? -%> + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '~> 2.13' + gem 'selenium-webdriver' + <%- end -%> end group :development do @@ -39,7 +45,7 @@ group :development do <%- end -%> <%- end -%> <% if depend_on_listen? -%> - gem 'listen', '~> 3.0.5' + gem 'listen', '>= 3.0.5', '< 3.2' <% end -%> <% if spring_install? -%> # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt index c88426ec06..4206002a1b 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt @@ -1,8 +1,8 @@ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's +// vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. JavaScript code in this file should be added after the last require_* statement. @@ -11,9 +11,8 @@ // about supported directives. // <% unless options[:skip_javascript] -%> -//= require <%= options[:javascript] %> -//= require <%= options[:javascript] %>_ujs -<% if gemfile_entries.any? { |m| m.name == "turbolinks" } -%> +//= require rails-ujs +<% unless options[:skip_turbolinks] -%> //= require turbolinks <% end -%> <% end -%> 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 index 0ebd7fe829..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 @@ -2,8 +2,8 @@ * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index d51f79bd49..5460155b3e 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -7,7 +7,7 @@ <%- if options[:skip_javascript] -%> <%%= stylesheet_link_tag 'application', media: 'all' %> <%- else -%> - <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%> + <%- unless options[:skip_turbolinks] -%> <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%- else -%> diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle index 1123dcf501..a84f0afe47 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/bundle +++ b/railties/lib/rails/generators/rails/app/templates/bin/bundle @@ -1,2 +1,2 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt index acae810c1a..560cc64a3f 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -3,7 +3,7 @@ require 'fileutils' include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") @@ -16,6 +16,11 @@ chdir APP_ROOT do puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') +<% unless options[:skip_yarn] %> + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') +<% end %> +<% unless options.skip_active_record -%> # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') @@ -24,6 +29,7 @@ chdir APP_ROOT do puts "\n== Preparing database ==" system! 'bin/rails db:setup' +<% end -%> puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update b/railties/lib/rails/generators/rails/app/templates/bin/update.tt index 770a605fed..0aedf0d6e2 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -3,7 +3,7 @@ require 'fileutils' include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") @@ -16,9 +16,11 @@ chdir APP_ROOT do puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') +<% unless options.skip_active_record -%> puts "\n== Updating database ==" system! 'bin/rails db:migrate' +<% end -%> puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn new file mode 100644 index 0000000000..44f75c22a4 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn @@ -0,0 +1,10 @@ +VENDOR_PATH = File.expand_path('..', __dir__) +Dir.chdir(VENDOR_PATH) do + begin + exec "yarnpkg #{ARGV.join(" ")}" + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index c0a0bd0a3e..0b1d22228e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -22,6 +22,9 @@ Bundler.require(*Rails.groups) module <%= app_const_base %> class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + 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. @@ -31,6 +34,10 @@ module <%= app_const_base %> # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true +<%- elsif !depends_on_system_test? -%> + + # Don't generate system test files. + config.generators.system_tests = nil <%- end -%> end 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 index 0bbde6f74f..1da4913082 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml @@ -7,3 +7,4 @@ test: production: adapter: redis url: redis://localhost:6379/1 + channel_prefix: <%= app_name %>_production 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 index a2b2a64ba6..8bc8735a8e 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 @@ -1,4 +1,4 @@ -# MySQL. Versions 5.0 and up are supported. +# MySQL. Versions 5.1.10 and up are supported. # # Install the MySQL driver: # gem install activerecord-jdbcmysql-adapter diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index d987cf303b..269af1470d 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 @@ -1,4 +1,4 @@ -# MySQL. Versions 5.0 and up are supported. +# MySQL. Versions 5.1.10 and up are supported. # # Install the MySQL driver # gem install mysql2 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 index d2499ea4fb..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 @@ -1,4 +1,4 @@ -# Oracle/OCI 8i, 9, 10g +# Oracle/OCI 11g or higher recommended # # Requires Ruby/OCI8: # https://github.com/kubo/ruby-oci8 @@ -17,7 +17,7 @@ # cursor_sharing: similar # default: &default - adapter: oracle + adapter: oracle_enhanced pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= app_name %> password: @@ -45,7 +45,9 @@ test: # On Heroku and other platform providers, you may have a full connection URL # available as an environment variable. For example: # -# DATABASE_URL="oracle://myuser:mypass@localhost/somedatabase" +# DATABASE_URL="oracle-enhanced://myuser:mypass@localhost/somedatabase" +# +# Note that the adapter name uses a dash instead of an underscore. # # You can use this database configuration with: # 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 index c223d6bc62..a21555e573 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 @@ -1,4 +1,4 @@ -# SQL Server (2005 or higher recommended) +# SQL Server (2012 or higher recommended) # # Install the adapters and driver # gem install tiny_tds @@ -8,29 +8,12 @@ # gem 'tiny_tds' # gem 'activerecord-sqlserver-adapter' # -# You should make sure freetds is configured correctly first. -# freetds.conf contains host/port/protocol_versions settings. -# http://freetds.schemamania.org/userguide/freetdsconf.htm -# -# A typical Microsoft server -# [mssql] -# host = mssqlserver.yourdomain.com -# port = 1433 -# tds version = 7.1 - -# If you can connect with "tsql -S servername", your basic FreeTDS installation is working. -# 'man tsql' for more info -# Set timeout to a larger number if valid queries against a live db fail -# default: &default adapter: sqlserver encoding: utf8 - pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - reconnect: false - username: <%= app_name %> - password: - timeout: 25 - dataserver: from_freetds.conf + username: sa + password: <%= ENV['SA_PASSWORD'] %> + host: localhost development: <<: *default 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 511b4a82eb..b75b65c8df 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 @@ -13,6 +13,7 @@ Rails.application.configure do config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true 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 7deab5dbb1..d44331a888 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 @@ -14,6 +14,11 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true + # Attempt to read encrypted secrets from `config/secrets.yml.enc`. + # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or + # `config/secrets.yml.key`. + config.read_encrypted_secrets = true + # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? @@ -31,8 +36,8 @@ Rails.application.configure do config.assets.compile = false # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - <%- end -%> + <%- end -%> # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' @@ -45,8 +50,8 @@ Rails.application.configure do # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] - <%- end -%> + <%- end -%> # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -63,14 +68,15 @@ Rails.application.configure do # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" + <%- unless options.skip_action_mailer? -%> config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false - <%- end -%> + <%- end -%> # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true @@ -88,7 +94,7 @@ Rails.application.configure do if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) + config.logger = ActiveSupport::TaggedLogging.new(logger) end <%- unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index 2318cf59ff..51196ae743 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -3,8 +3,12 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path +# Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path +<%- unless options[:skip_yarn] -%> +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') +<%- end -%> # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt deleted file mode 100644 index 5ad18cc5ad..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt +++ /dev/null @@ -1,38 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.0 upgrade. -# -<%- if options[:update] -%> -# Once upgraded flip defaults one by one to migrate to the new default. -# -<%- end -%> -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -<%- unless options[:api] -%> - -# Enable per-form CSRF tokens. Previous versions had false. -Rails.application.config.action_controller.per_form_csrf_tokens = <%= options[:update] ? false : true %> - -# Enable origin-checking CSRF mitigation. Previous versions had false. -Rails.application.config.action_controller.forgery_protection_origin_check = <%= options[:update] ? false : true %> -<%- end -%> - -# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. -# Previous versions had false. -ActiveSupport.to_time_preserves_timezone = <%= options[:update] ? false : true %> -<%- unless options[:skip_active_record] -%> - -# Require `belongs_to` associations by default. Previous versions had false. -Rails.application.config.active_record.belongs_to_required_by_default = <%= options[:update] ? false : true %> -<%- end -%> - -# Do not halt callback chains when a callback returns false. Previous versions had true. -ActiveSupport.halt_callback_chains_on_return_false = <%= options[:update] ? true : false %> -<%- unless options[:update] -%> - -# Configure SSL options to enable HSTS with subdomains. Previous versions had false. -Rails.application.config.ssl_options = { hsts: { subdomains: true } } -<%- end -%> - -# Unknown asset fallback will return the path passed in when the given -# asset is not present in the asset pipeline. -Rails.application.config.assets.unknown_asset_fallback = <%= options[:update] ? true : false %> 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 new file mode 100644 index 0000000000..900baa607a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt @@ -0,0 +1,15 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.2 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Make Active Record use stable #cache_key alongside new #cache_version method. +# This is needed for recyclable cache keys. +# Rails.application.config.active_record.cache_versioning = true + +# 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 diff --git a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml index 0653957166..decc5a8573 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml @@ -16,6 +16,16 @@ # # This would use the information in config/locales/es.yml. # +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb index 7ee948002e..1e19380dcb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb @@ -1,13 +1,13 @@ # Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers a minimum and maximum. +# The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum, this matches the default thread size of Active Record. +# and maximum; this matches the default thread size of Active Record. # threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } threads threads_count, threads_count -# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT") { 3000 } @@ -42,9 +42,9 @@ environment ENV.fetch("RAILS_ENV") { "development" } # 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 `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, Ruby +# 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 diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index 8e995a5df1..ea9d47396c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -12,8 +12,8 @@ # Shared secrets are available across all environments. -shared: - api_key: 123 +# shared: +# api_key: a1B2c3D4e5F6 # Environmental secrets are only available for that specific environment. @@ -23,8 +23,10 @@ development: test: secret_key_base: <%= app_secret %> -# Do not keep production secrets in the repository, -# instead read values from the environment. +# Do not keep production secrets in the unencrypted secrets file. +# Instead, either read values from the environment. +# Or, use `bin/rails secrets:setup` to configure encrypted secrets +# and move the `production:` environment over there. production: secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 0e66cc4237..7221c26729 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -21,5 +21,9 @@ !/tmp/.keep <% end -%> -# Ignore Byebug command history file. +<% unless options[:skip_yarn] -%> +/node_modules +/yarn-error.log + +<% end -%> .byebug_history diff --git a/railties/lib/rails/generators/rails/app/templates/package.json b/railties/lib/rails/generators/rails/app/templates/package.json new file mode 100644 index 0000000000..46db57dcbe --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/package.json @@ -0,0 +1,5 @@ +{ + "name": "<%= app_name %>", + "private": true, + "dependencies": {} +} diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html index b612547fc2..2be3af26fc 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/404.html +++ b/railties/lib/rails/generators/rails/app/templates/public/404.html @@ -4,7 +4,7 @@ <title>The page you were looking for doesn't exist (404)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/404.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html index a21f82b3bd..c08eac0d1d 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/422.html +++ b/railties/lib/rails/generators/rails/app/templates/public/422.html @@ -4,7 +4,7 @@ <title>The change you wanted was rejected (422)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/422.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html index 061abc587d..78a030af22 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/500.html +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -4,7 +4,7 @@ <title>We're sorry, but something went wrong (500)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/500.html --> <div class="dialog"> <div> 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 new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end 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 index 2f92168eef..7568af5b5e 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 @@ -1,4 +1,4 @@ -require File.expand_path('../../config/environment', __FILE__) +require File.expand_path('../config/environment', __dir__) require 'rails/test_help' class ActiveSupport::TestCase diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb index 265dada2ca..95d00c2d39 100644 --- a/railties/lib/rails/generators/rails/assets/assets_generator.rb +++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb @@ -7,7 +7,7 @@ module Rails class_option :javascript_engine, desc: "Engine for JavaScripts" class_option :stylesheet_engine, desc: "Engine for Stylesheets" - protected + private def asset_name file_name diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index 213de37cce..06bdb8b5ce 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -3,6 +3,8 @@ module Rails class ControllerGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb." + class_option :helper, type: :boolean + class_option :assets, type: :boolean check_class_collision suffix: "Controller" @@ -14,7 +16,7 @@ module Rails unless options[:skip_routes] actions.reverse_each do |action| # route prepends two spaces onto the front of the string that is passed, this corrects that. - route generate_routing_code(action)[2..-1] + route indent(generate_routing_code(action), 2)[2..-1] end end end @@ -32,27 +34,30 @@ module Rails # end # end def generate_routing_code(action) - depth = regular_class_path.length + depth = 0 + lines = [] + # Create 'namespace' ladder # namespace :foo do # namespace :bar do - namespace_ladder = regular_class_path.each_with_index.map do |ns, i| - indent(" namespace :#{ns} do\n", i * 2) - end.join + regular_class_path.each do |ns| + lines << indent("namespace :#{ns} do\n", depth * 2) + depth += 1 + end # Create route # get 'baz/index' - route = indent(%{ get '#{file_name}/#{action}'\n}, depth * 2) + lines << indent(%{get '#{file_name}/#{action}'\n}, depth * 2) # Create `end` ladder # end # end - end_ladder = (1..depth).reverse_each.map do |i| - indent("end\n", i * 2) - end.join + until depth.zero? + depth -= 1 + lines << indent("end\n", depth * 2) + end - # Combine the 3 parts to generate complete route entry - namespace_ladder + route + end_ladder + lines.join 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 new file mode 100644 index 0000000000..1da2fbc1a5 --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb @@ -0,0 +1,70 @@ +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/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb index 8040ec5e7b..299a7da5f1 100644 --- a/railties/lib/rails/generators/rails/generator/generator_generator.rb +++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb @@ -12,7 +12,7 @@ module Rails hook_for :test_framework - protected + private def generator_dir if options[:namespace] diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt index d0575772bc..178d5c3f9f 100644 --- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt +++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt @@ -1,3 +1,3 @@ class <%= class_name %>Generator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 9ffeab4fbe..118e44d9d0 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -81,7 +81,7 @@ task default: :test end PASSTHROUGH_OPTIONS = [ - :skip_active_record, :skip_action_mailer, :skip_javascript, :database, + :skip_active_record, :skip_action_mailer, :skip_javascript, :skip_sprockets, :database, :javascript, :quiet, :pretend, :force, :skip ] @@ -91,6 +91,8 @@ task default: :test opts[:skip_bundle] = true opts[:api] = options.api? opts[:skip_listen] = true + opts[:skip_git] = true + opts[:skip_turbolinks] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -112,7 +114,6 @@ task default: :test def test_dummy_clean inside dummy_path do - remove_file ".gitignore" remove_file "db/seeds.rb" remove_file "doc" remove_file "Gemfile" @@ -186,7 +187,7 @@ task default: :test desc: "Skip gemspec file" class_option :skip_gemfile_entry, type: :boolean, default: false, - desc: "If creating plugin in application's directory " + + desc: "If creating plugin in application's directory " \ "skip adding entry to Gemfile" class_option :api, type: :boolean, default: false, @@ -195,10 +196,6 @@ task default: :test def initialize(*args) @dummy_path = nil super - - unless plugin_path - raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help" - end end public_task :set_default_accessors! @@ -270,8 +267,8 @@ task default: :test @name ||= begin # same as ActiveSupport::Inflector#underscore except not replacing '-' underscored = original_name.dup - underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') - underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + underscored.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + underscored.gsub!(/([a-z\d])([A-Z])/, '\1_\2') underscored.downcase! underscored @@ -283,10 +280,10 @@ task default: :test end def namespaced_name - @namespaced_name ||= name.gsub("-", "/") + @namespaced_name ||= name.tr("-", "/") end - protected + private def create_dummy_app(path = nil) dummy_path(path) if path @@ -304,7 +301,7 @@ task default: :test end def engine? - full? || mountable? + full? || mountable? || options[:engine] end def full? @@ -415,7 +412,6 @@ task default: :test require 'rake/testtask' Rake::TestTask.new(:test) do |t| - t.libs << 'lib' t.libs << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = false @@ -437,7 +433,7 @@ end end def inside_application? - rails_app_path && app_path =~ /^#{rails_app_path}/ + rails_app_path && destination_root.start_with?(rails_app_path.to_s) end def relative_path diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec index d84d1aabdb..9a8c4bf098 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec @@ -1,4 +1,4 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path("lib", __dir__) # Maintain your gem's version: require "<%= namespaced_name %>/version" diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index 383d2fb2d1..3581dd401a 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -15,7 +15,7 @@ RDoc::Task.new(:rdoc) do |rdoc| end <% if engine? && !options[:skip_active_record] && with_dummy_app? -%> -APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path("<%= dummy_path -%>/Rakefile", __dir__) load 'rails/tasks/engine.rake' <% end %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index 56e7925c6b..ffa277e334 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -1,11 +1,12 @@ # This command will automatically be run when you run "rails" with Rails gems # installed from the root of your application. -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__) +ENGINE_ROOT = File.expand_path('..', __dir__) +ENGINE_PATH = File.expand_path('../lib/<%= namespaced_name -%>/engine', __dir__) +APP_PATH = File.expand_path('../<%= dummy_path -%>/config/application', __dir__) # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) require 'rails/all' diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt index 62b94618fd..8e7d321626 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -1,8 +1,4 @@ -$: << File.expand_path(File.expand_path('../../test', __FILE__)) +$: << File.expand_path("../test", __dir__) -require 'bundler/setup' -require 'rails/test_unit/minitest_plugin' - -Rails::TestUnitReporter.executable = 'bin/test' - -exit Minitest.run(ARGV) +require "bundler/setup" +require "rails/plugin/test" 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 new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end 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 index e84e403018..32e8202e1c 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 @@ -1,8 +1,8 @@ -require File.expand_path("../../<%= options[:dummy_path] -%>/config/environment.rb", __FILE__) +require File.expand_path("../<%= options[:dummy_path] -%>/config/environment.rb", __dir__) <% unless options[:skip_active_record] -%> -ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../<%= options[:dummy_path] -%>/db/migrate", __FILE__)] +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)] <% if options[:mountable] -%> -ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) +ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) <% end -%> <% end -%> require "rails/test_help" @@ -17,7 +17,7 @@ Rails::TestUnitReporter.executable = 'bin/test' # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index ed6bf7f7d7..12d6bc85b2 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -6,6 +6,7 @@ module Rails remove_hook_for :resource_controller remove_class_option :actions + class_option :api, type: :boolean class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" class_option :stylesheet_engine, desc: "Engine for Stylesheets" class_option :assets, type: :boolean @@ -15,10 +16,13 @@ module Rails def handle_skip @options = @options.merge(stylesheets: false) unless options[:assets] @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] && options[:scaffold_stylesheet] + @options = @options.merge(system_tests: false) if options[:api] end hook_for :scaffold_controller, required: true + hook_for :system_tests, as: :system + hook_for :assets do |assets| invoke assets, [controller_name] end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index e4f3161ffd..cf97c22160 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -20,7 +20,11 @@ module Rails template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb") end - hook_for :template_engine, :test_framework, as: :scaffold + hook_for :template_engine, as: :scaffold do |template_engine| + invoke template_engine unless options.api? + end + + hook_for :test_framework, as: :scaffold # Invoke the helper using the controller name (pluralized) hook_for :helper, as: :scaffold do |invoked| diff --git a/railties/lib/rails/generators/rails/system_test/USAGE b/railties/lib/rails/generators/rails/system_test/USAGE new file mode 100644 index 0000000000..f11a99e008 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/USAGE @@ -0,0 +1,10 @@ +Description: + Stubs out a new system test. Pass the name of the test, either + CamelCased or under_scored, as an argument. + + This generator invokes the current system tool, which defaults to + TestUnit. + +Example: + `rails generate system_test GeneralStories` creates a GeneralStories + system test in test/system/general_stories_test.rb diff --git a/railties/lib/rails/generators/rails/system_test/system_test_generator.rb b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb new file mode 100644 index 0000000000..901120e892 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb @@ -0,0 +1,7 @@ +module Rails + module Generators + class SystemTestGenerator < NamedBase # :nodoc: + hook_for :system_tests, as: :system + end + end +end diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 6d80003271..e7cb722473 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -23,10 +23,14 @@ module Rails assign_controller_names!(controller_name.pluralize) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :controller_name, :controller_file_name + private + def controller_class_path if options[:model_name] @controller_class_path @@ -55,7 +59,7 @@ module Rails end # Loads the ORM::Generators::ActiveModel class. This class is responsible - # to tell scaffold entities how to generate an specific method for the + # to tell scaffold entities how to generate a specific method for the # ORM. Check Rails::Generators::ActiveModel for more information. def orm_class @orm_class ||= begin @@ -73,7 +77,7 @@ module Rails end # Initialize ORM::Generators::ActiveModel to access instance methods. - def orm_instance(name=singular_table_name) + def orm_instance(name = singular_table_name) @orm_instance ||= orm_class.new(name) end end diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb index 3eec929aeb..575af80303 100644 --- a/railties/lib/rails/generators/test_case.rb +++ b/railties/lib/rails/generators/test_case.rb @@ -14,7 +14,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # end # # If you want to ensure your destination root is clean before running each test, @@ -22,7 +22,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # setup :prepare_destination # end class TestCase < ActiveSupport::TestCase diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb index 59f8d40343..6b6e094453 100644 --- a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb +++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb @@ -12,7 +12,7 @@ module TestUnit # :nodoc: template "generator_test.rb", File.join("test/lib/generators", class_path, "#{file_name}_generator_test.rb") end - protected + private def generator_path if options[:namespace] 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 index dea7e22196..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 @@ -1,7 +1,9 @@ require 'test_helper' +<% module_namespacing do -%> class <%= class_name %>Test < ActionDispatch::IntegrationTest # test "the truth" do # assert true # end end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 806279788e..67bff8e4f9 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -17,7 +17,7 @@ module TestUnit # :nodoc: template "preview.rb", File.join("test/mailers/previews", class_path, "#{file_name}_mailer_preview.rb") end - protected + private def file_name @_file_name ||= super.gsub(/_mailer/i, "") end 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 8840a86d0d..292db35121 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -22,7 +22,7 @@ module TestUnit # :nodoc: def fixture_name @fixture_name ||= if mountable_engine? - "%s_%s" % [namespaced_path, table_name] + (namespace_dirs + [table_name]).join("_") else table_name end diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb new file mode 100644 index 0000000000..0514957d9c --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb @@ -0,0 +1,17 @@ +require "rails/generators/test_unit" + +module TestUnit # :nodoc: + module Generators # :nodoc: + class SystemGenerator < Base # :nodoc: + check_class_collision suffix: "Test" + + def create_test_files + if !File.exist?(File.join("test/application_system_test_case.rb")) + template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb") + end + + template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb") + end + end + end +end 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 new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end 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 new file mode 100644 index 0000000000..b5ce2ba5c8 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb @@ -0,0 +1,9 @@ +require "application_system_test_case" + +class <%= class_name.pluralize %>Test < ApplicationSystemTestCase + # test "visiting the index" do + # visit <%= plural_table_name %>_url + # + # assert_selector "h1", text: "<%= class_name %>" + # end +end diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index a1e5a233b9..ce0e42e60d 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -14,12 +14,12 @@ module Rails include ActiveSupport::Testing::Stream included do - class_attribute :destination_root, :current_path, :generator_class, :default_arguments - # Generators frequently change the current path using +FileUtils.cd+. # So we need to store the path at file load and revert back to it after each test. - self.current_path = File.expand_path(Dir.pwd) - self.default_arguments = [] + class_attribute :current_path, default: File.expand_path(Dir.pwd) + class_attribute :default_arguments, default: [] + class_attribute :destination_root + class_attribute :generator_class end module ClassMethods @@ -40,7 +40,7 @@ module Rails # Sets the destination of generator files: # - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) def destination(path) self.destination_root = path end @@ -51,7 +51,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # setup :prepare_destination # # test "database.yml is not created when skipping Active Record" do @@ -82,23 +82,23 @@ module Rails Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(":")) end - protected + private - def destination_root_is_set? # :nodoc: + def destination_root_is_set? raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root end - def ensure_current_path # :nodoc: + def ensure_current_path cd current_path end # Clears all files and directories in destination. - def prepare_destination + def prepare_destination # :doc: rm_rf(destination_root) mkdir_p(destination_root) end - def migration_file_name(relative) # :nodoc: + def migration_file_name(relative) absolute = File.expand_path(relative, destination_root) dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, "") Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index 5d4acd6f6b..fc064dac32 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -1,9 +1,9 @@ require "cgi" module Rails - # This module helps build the runtime properties used to display in the - # Rails::InfoController responses. Including the active Rails version, Ruby - # version, Rack version, and so on. + # This module helps build the runtime properties that are displayed in + # Rails::InfoController responses. These include the active Rails version, + # Ruby version, Rack version, and so on. module Info mattr_accessor :properties class << (@@properties = []) diff --git a/railties/lib/rails/initializable.rb b/railties/lib/rails/initializable.rb index 81b1cd8110..a2615d5efd 100644 --- a/railties/lib/rails/initializable.rb +++ b/railties/lib/rails/initializable.rb @@ -53,7 +53,7 @@ module Rails end end - def run_initializers(group=:default, *args) + def run_initializers(group = :default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 95de998208..7ff55ef5bf 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -6,6 +6,8 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: before_action :require_local!, unless: :show_previews? before_action :find_preview, only: :preview + helper_method :part_query + def index @previews = ActionMailer::Preview.all @page_title = "Mailer Previews" @@ -19,7 +21,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @email_action = File.basename(params[:path]) if @preview.email_exists?(@email_action) - @email = @preview.call(@email_action) + @email = @preview.call(@email_action, params) if params[:part] part_type = Mime::Type.lookup(params[:part]) @@ -40,12 +42,12 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end end - protected - def show_previews? + private + def show_previews? # :doc: ActionMailer::Base.show_previews end - def find_preview + def find_preview # :doc: candidates = [] params[:path].to_s.scan(%r{/|$}) { candidates << $` } preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) } @@ -57,7 +59,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end end - def find_preferred_part(*formats) + def find_preferred_part(*formats) # :doc: formats.each do |format| if part = @email.find_first_mime_type(format) return part @@ -69,11 +71,15 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end end - def find_part(format) + def find_part(format) # :doc: if part = @email.find_first_mime_type(format) part elsif @email.mime_type == format @email end end + + def part_query(mime_type) + request.query_parameters.merge(part: mime_type).to_query + end end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 1c1810dde6..6bdb673215 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -2,12 +2,12 @@ module Rails module Paths # This object is an extended hash that behaves as root of the <tt>Rails::Paths</tt> system. # It allows you to collect information about how you want to structure your application - # paths by a Hash like API. It requires you to give a physical path on initialization. + # paths through a Hash-like API. It requires you to give a physical path on initialization. # # root = Root.new "/rails" # root.add "app/controllers", eager_load: true # - # The command above creates a new root object and adds "app/controllers" as a path. + # The above command creates a new root object and adds "app/controllers" as a path. # This means we can get a <tt>Rails::Paths::Path</tt> object back like below: # # path = root["app/controllers"] @@ -45,7 +45,6 @@ module Rails attr_accessor :path def initialize(path) - @current = nil @path = path @root = {} end @@ -206,7 +205,14 @@ module Rails # Returns all expanded paths but only if they exist in the filesystem. def existent - expanded.select { |f| File.exist?(f) } + expanded.select do |f| + does_exist = File.exist?(f) + + if !does_exist && File.symlink?(f) + raise "File #{f.inspect} is a symlink that does not point to a valid file" + end + does_exist + end end def existent_directories diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb new file mode 100644 index 0000000000..ff043b488e --- /dev/null +++ b/railties/lib/rails/plugin/test.rb @@ -0,0 +1,7 @@ +require "rails/test_unit/minitest_plugin" + +Rails::TestUnitReporter.executable = "bin/test" + +Minitest.run_via = :rails + +require "active_support/testing/autorun" diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb deleted file mode 100644 index dccfa3e9bb..0000000000 --- a/railties/lib/rails/rack/debugger.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "active_support/deprecation" - -ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index e3fee75603..853fc26051 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -27,45 +27,43 @@ module Rails end end - protected + private - def call_app(request, env) - instrumenter = ActiveSupport::Notifications.instrumenter - instrumenter.start "request.action_dispatch", request: request - logger.info { started_request_message(request) } - resp = @app.call(env) - resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } - resp - rescue Exception - finish(request) - raise - ensure - ActiveSupport::LogSubscriber.flush_all! - end + def call_app(request, env) # :doc: + instrumenter = ActiveSupport::Notifications.instrumenter + instrumenter.start "request.action_dispatch", request: request + logger.info { started_request_message(request) } + resp = @app.call(env) + resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } + resp + rescue Exception + finish(request) + raise + ensure + ActiveSupport::LogSubscriber.flush_all! + end - # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700 - def started_request_message(request) - 'Started %s "%s" for %s at %s' % [ - request.request_method, - request.filtered_path, - request.ip, - Time.now.to_default_s ] - end + # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700 + def started_request_message(request) # :doc: + 'Started %s "%s" for %s at %s' % [ + request.request_method, + request.filtered_path, + request.ip, + Time.now.to_default_s ] + end - def compute_tags(request) - @taggers.collect do |tag| - case tag - when Proc - tag.call(request) - when Symbol - request.send(tag) - else - tag + def compute_tags(request) # :doc: + @taggers.collect do |tag| + case tag + when Proc + tag.call(request) + when Symbol + request.send(tag) + else + tag + end end end - end - - private def finish(request) instrumenter = ActiveSupport::Notifications.instrumenter diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index eb3f5d4ee9..3476bb0eb5 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -132,27 +132,19 @@ module Rails end def rake_tasks(&blk) - @rake_tasks ||= [] - @rake_tasks << blk if blk - @rake_tasks + register_block_for(:rake_tasks, &blk) end def console(&blk) - @load_console ||= [] - @load_console << blk if blk - @load_console + register_block_for(:load_console, &blk) end def runner(&blk) - @load_runner ||= [] - @load_runner << blk if blk - @load_runner + register_block_for(:runner, &blk) end def generators(&blk) - @generators ||= [] - @generators << blk if blk - @generators + register_block_for(:generators, &blk) end def abstract_railtie? @@ -170,10 +162,6 @@ module Rails @instance ||= new end - def respond_to_missing?(*args) - instance.respond_to?(*args) || super - end - # Allows you to configure the railtie. This is the same method seen in # Railtie::Configurable, but this module is no longer required for all # subclasses of Railtie so we provide the class method here. @@ -181,11 +169,15 @@ module Rails instance.configure(&block) end - protected - def generate_railtie_name(string) #:nodoc: + private + def generate_railtie_name(string) ActiveSupport::Inflector.underscore(string).tr("/", "_") end + def respond_to_missing?(name, _) + instance.respond_to?(name) || super + end + # If the class method does not have a method, then send the method call # to the Railtie instance. def method_missing(name, *args, &block) @@ -195,6 +187,16 @@ module Rails super end end + + # receives an instance variable identifier, set the variable value if is + # blank and append given block to value, which will be used later in + # `#each_registered_block(type, &block)` + def register_block_for(type, &blk) + var_name = "@#{type}" + blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, []) + blocks << blk if blk + blocks + end end delegate :railtie_name, to: :class @@ -241,6 +243,7 @@ module Rails private + # run `&block` in every registered block in `#register_block_for` def each_registered_block(type, &block) klass = self.class while klass.respond_to?(type) diff --git a/railties/lib/rails/railtie/configurable.rb b/railties/lib/rails/railtie/configurable.rb index 39f1f87575..2a8295426e 100644 --- a/railties/lib/rails/railtie/configurable.rb +++ b/railties/lib/rails/railtie/configurable.rb @@ -24,7 +24,7 @@ module Rails class_eval(&block) end - protected + private def method_missing(*args, &block) instance.send(*args, &block) diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb new file mode 100644 index 0000000000..c7a8676d7b --- /dev/null +++ b/railties/lib/rails/secrets.rb @@ -0,0 +1,118 @@ +require "yaml" +require "active_support/message_encryptor" +require "active_support/core_ext/string/strip" + +module Rails + # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 + class Secrets # :nodoc: + class MissingKeyError < RuntimeError + def initialize + super(<<-end_of_message.squish) + Missing encryption key to decrypt secrets with. + Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"] + end_of_message + end + end + + @cipher = "aes-128-gcm" + @root = File # Wonky, but ensures `join` uses the current directory. + + class << self + attr_writer :root + + def parse(paths, env:) + paths.each_with_object(Hash.new) do |path, all_secrets| + require "erb" + + secrets = YAML.load(ERB.new(preprocess(path)).result) || {} + all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"] + all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env] + 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 + + def decrypt(data) + encryptor.decrypt_and_verify(data) + end + + def read + decrypt(IO.binread(path)) + end + + def write(contents) + IO.binwrite("#{path}.tmp", encrypt(contents)) + FileUtils.mv("#{path}.tmp", path) + end + + def read_for_editing(&block) + writing(read, &block) + end + + def read_template_for_editing(&block) + writing(template, &block) + end + + private + def handle_missing_key + raise MissingKeyError + end + + def read_key_file + if File.exist?(key_path) + IO.binread(key_path).strip + end + end + + def key_path + @root.join("config", "secrets.yml.key") + end + + def path + @root.join("config", "secrets.yml.enc").to_s + end + + def preprocess(path) + if path.end_with?(".enc") + decrypt(IO.binread(path)) + else + IO.read(path) + end + end + + def writing(contents) + tmp_path = File.join(Dir.tmpdir, File.basename(path)) + File.write(tmp_path, contents) + + yield tmp_path + + write(File.read(tmp_path)) + ensure + FileUtils.rm(tmp_path) if File.exist?(tmp_path) + end + + def encryptor + @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher) + end + end + end +end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index f0df76d3f3..e9088c44ce 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -13,7 +13,7 @@ # start with the tag optionally followed by a colon. Everything up to the end # of the line (or closing ERB comment tag) is considered to be their text. class SourceAnnotationExtractor - class Annotation < Struct.new(:line, :tag, :text) + Annotation = Struct.new(:line, :tag, :text) do def self.directories @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") end @@ -44,7 +44,7 @@ class SourceAnnotationExtractor # # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. # Otherwise the string contains just line and text. - def to_s(options={}) + def to_s(options = {}) s = "[#{line.to_s.rjust(options[:indent])}] " s << "[#{tag}] " if options[:tag] s << text @@ -66,7 +66,7 @@ class SourceAnnotationExtractor # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. # # This class method is the single entry point for the rake tasks. - def self.enumerate(tag, options={}) + def self.enumerate(tag, options = {}) extractor = new(tag) dirs = options.delete(:dirs) || Annotation.directories extractor.display(extractor.find(dirs), options) @@ -116,7 +116,7 @@ class SourceAnnotationExtractor # Otherwise it returns an empty hash. def extract_annotations_from(file, pattern) lineno = 0 - result = File.readlines(file).inject([]) do |list, line| + result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line| lineno += 1 next list unless line =~ pattern list << Annotation.new(lineno, $1, $2) @@ -126,7 +126,7 @@ class SourceAnnotationExtractor # Prints the mapping from filenames to annotations in +results+ ordered by filename. # The +options+ hash is passed to each annotation's +to_s+. - def display(results, options={}) + def display(results, options = {}) options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size results.keys.sort.each do |file| puts "#{file}:" diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 48675b3845..c0a50e5bda 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -12,6 +12,7 @@ require "rake" restart routes tmp + yarn ).tap { |arr| arr << "statistics" if Rake.application.current_scope.empty? }.each do |task| diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake index c92b42f6c1..177b138090 100644 --- a/railties/lib/rails/tasks/engine.rake +++ b/railties/lib/rails/tasks/engine.rake @@ -1,6 +1,17 @@ task "load_app" do namespace :app do load APP_RAKEFILE + + desc "Update some initially generated files" + task update: [ "update:bin" ] + + namespace :update do + require "rails/engine/updater" + # desc "Adds new executables to the engine bin/ directory" + task :bin do + Rails::Engine::Updater.run(:create_bin_files) + end + end end task environment: "app:environment" diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index a2167796eb..80720a42ff 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -1,5 +1,3 @@ -require "active_support/deprecation" - namespace :app do desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ] @@ -18,7 +16,7 @@ namespace :app do namespace :templates do # desc "Copy all the templates from rails to the application directory for customization. Already existing local copies will be overwritten" task :copy do - generators_lib = File.expand_path("../../generators", __FILE__) + generators_lib = File.expand_path("../generators", __dir__) project_templates = "#{Rails.root}/lib/templates" default_templates = { "erb" => %w{controller mailer scaffold}, @@ -65,7 +63,7 @@ namespace :app do # desc "Adds new executables to the application bin/ directory" task :bin do - RailsUpdate.invoke_from_app_generator :create_bin_files + RailsUpdate.invoke_from_app_generator :update_bin_files end task :upgrade_guide_info do @@ -73,15 +71,3 @@ namespace :app do end end end - -namespace :rails do - %i(update template templates:copy update:configs update:bin).each do |task_name| - task "#{task_name}" do - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace. - Run bin/rails app:#{task_name} instead. - MSG - Rake.application.invoke_task("app:#{task_name}") - end - end -end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake index c376234fee..ba796845d7 100644 --- a/railties/lib/rails/tasks/log.rake +++ b/railties/lib/rails/tasks/log.rake @@ -3,7 +3,7 @@ namespace :log do ## # Truncates all/specified log files # ENV['LOGS'] - # - defaults to standard environment log files i.e. 'development,test,production' + # - defaults to all environments log files i.e. 'development,test,production' # - ENV['LOGS']=all truncates all files i.e. log/*.log # - ENV['LOGS']='test,development' truncates only specified files desc "Truncates all/specified *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" @@ -19,7 +19,7 @@ namespace :log do elsif ENV["LOGS"] log_files_to_truncate(ENV["LOGS"]) else - log_files_to_truncate("development,test,production") + log_files_to_truncate(all_environments.join(",")) end end @@ -33,4 +33,8 @@ namespace :log do f = File.open(file, "w") f.close end + + def all_environments + Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") } + end end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index f5e5b9ae87..215fb2ceb5 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -1,5 +1,3 @@ -require "active_support/deprecation" -require "active_support/core_ext/string/strip" # for strip_heredoc require "optparse" desc "Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option" @@ -7,15 +5,8 @@ task routes: :environment do all_routes = Rails.application.routes.routes require "action_dispatch/routing/inspector" inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes) - if ARGV.any? { |argv| argv.start_with? "CONTROLLER" } - puts <<-eow.strip_heredoc - Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1. - Please use `bin/rails routes -c controller_name` instead. - eow - end routes_filter = nil - routes_filter = { controller: ENV["CONTROLLER"] } if ENV["CONTROLLER"] OptionParser.new do |opts| opts.banner = "Usage: rails routes [options]" diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index a6cdd1e99c..cb569be58b 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -8,9 +8,8 @@ STATS_DIRECTORIES = [ %w(Models app/models), %w(Mailers app/mailers), %w(Channels app/channels), - %w(Javascripts app/assets/javascripts), + %w(JavaScripts app/assets/javascripts), %w(Libraries lib/), - %w(Tasks lib/tasks), %w(APIs app/apis), %w(Controller\ tests test/controllers), %w(Helper\ tests test/helpers), @@ -18,6 +17,7 @@ STATS_DIRECTORIES = [ %w(Mailer\ tests test/mailers), %w(Job\ tests test/jobs), %w(Integration\ tests test/integration), + %w(System\ tests test/system), ].collect do |name, dir| [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] end.select { |name, dir| File.directory?(dir) } diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake new file mode 100644 index 0000000000..87476b1b8c --- /dev/null +++ b/railties/lib/rails/tasks/yarn.rake @@ -0,0 +1,11 @@ +namespace :yarn do + desc "Install all JavaScript dependencies as specified via Yarn" + task :install do + system("./bin/yarn install --no-progress") + end +end + +# Run Yarn prior to Sprockets assets precompilation, so dependencies are available for use. +if Rake::Task.task_defined?("assets:precompile") + Rake::Task["assets:precompile"].enhance [ "yarn:install" ] +end diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index c63781ed0c..89c1129f90 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -98,8 +98,8 @@ <% if @email.multipart? %> <dd> <select onchange="formatChanged(this);"> - <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> - <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + <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> <% end %> @@ -107,7 +107,7 @@ </header> <% if @part && @part.mime_type %> - <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> + <iframe seamless name="messageBody" src="?<%= part_query(@part.mime_type) %>"></iframe> <% else %> <p> You are trying to preview an email that does not have any content. diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index db341dd847..81537d813e 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -12,12 +12,19 @@ require "rails/generators/test_case" require "active_support/testing/autorun" if defined?(ActiveRecord::Base) - ActiveRecord::Migration.maintain_test_schema! + begin + ActiveRecord::Migration.maintain_test_schema! + rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 + end - class ActiveSupport::TestCase - include ActiveRecord::TestFixtures - self.fixture_path = "#{Rails.root}/test/fixtures/" - self.file_fixture_path = self.fixture_path + "files" + module ActiveSupport + class TestCase + include ActiveRecord::TestFixtures + self.fixture_path = "#{Rails.root}/test/fixtures/" + self.file_fixture_path = fixture_path + "files" + end end ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path @@ -27,6 +34,8 @@ if defined?(ActiveRecord::Base) end end +# :enddoc: + class ActionController::TestCase def before_setup # :nodoc: @routes = Rails.application.routes diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 6e196a32ab..7a852d1109 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -6,7 +6,7 @@ require "shellwords" module Minitest class SuppressedSummaryReporter < SummaryReporter # Disable extra failure output after a run if output is inline. - def aggregated_results + def aggregated_results(*) super unless options[:output_inline] end end @@ -52,21 +52,25 @@ module Minitest options[:color] = value end + opts.on("-w", "--warnings", + "Enable ruby warnings") do + $VERBOSE = true + end + options[:color] = true options[:output_inline] = true - options[:patterns] = defined?(@rake_patterns) ? @rake_patterns : opts.order! + options[:patterns] = opts.order! unless run_via.rake? end - # Running several Rake tasks in a single command would trip up the runner, - # as the patterns would also contain the other Rake tasks. - def self.rake_run(patterns) # :nodoc: - @rake_patterns = patterns + def self.rake_run(patterns, exclude_patterns = []) # :nodoc: + self.run_via = :rake unless run_via.set? + ::Rails::TestRequirer.require_files(patterns, exclude_patterns) autorun end module RunRespectingRakeTestopts def run(args = []) - if defined?(@rake_patterns) + if run_via.rake? args = Shellwords.split(ENV["TESTOPTS"] || "") end @@ -81,9 +85,16 @@ module Minitest def self.plugin_rails_init(options) ENV["RAILS_ENV"] = options[:environment] || "test" - # If run via `ruby` we've been passed the files to run directly. - unless run_via[:ruby] - ::Rails::TestRequirer.require_files(options[:patterns]) + # If run via `ruby` we've been passed the files to run directly, or if run + # via `rake` then they have already been eagerly required. + unless run_via.ruby? || run_via.rake? + # If there are no given patterns, we can assume that the user + # simply runs the `bin/rails test` command without extra arguments. + if options[:patterns].empty? + ::Rails::TestRequirer.require_files(options[:patterns], ["test/system/**/*"]) + else + ::Rails::TestRequirer.require_files(options[:patterns]) + end end unless options[:full_backtrace] || ENV["BACKTRACE"] @@ -97,7 +108,33 @@ module Minitest reporter << ::Rails::TestUnitReporter.new(options[:io], options) end - mattr_accessor(:run_via) { Hash.new } + def self.run_via=(runner) + if run_via.set? + raise ArgumentError, "run_via already assigned" + else + run_via.runner = runner + end + end + + class RunVia + attr_accessor :runner + alias set? runner + + # Backwardscompatibility with Rails 5.0 generated plugin test scripts. + def []=(runner, *) + @runner = runner + end + + def ruby? + runner == :ruby + end + + def rake? + runner == :rake + end + end + + mattr_reader(:run_via) { RunVia.new } end # Put Rails as the first plugin minitest initializes so other plugins diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index d0fc795515..443e743421 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,7 +1,7 @@ require "rails/test_unit/line_filtering" if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? - ENV["RAILS_ENV"] ||= "test" + ENV["RAILS_ENV"] ||= Rake.application.options.show_tasks ? "development" : "test" end module Rails @@ -11,10 +11,13 @@ module Rails fixture_replacement: nil c.integration_tool :test_unit + c.system_tests :test_unit end initializer "test_unit.line_filtering" do - ActiveSupport::TestCase.extend Rails::LineFiltering + ActiveSupport.on_load(:active_support_test_case) { + ActiveSupport::TestCase.extend Rails::LineFiltering + } end rake_tasks do diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index fe11664d5e..1cc27f7b6c 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -3,8 +3,7 @@ require "minitest" module Rails class TestUnitReporter < Minitest::StatisticsReporter - class_attribute :executable - self.executable = "bin/rails test" + class_attribute :executable, default: "bin/rails test" def record(result) super diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb index fe35934abc..92e5fcf0bc 100644 --- a/railties/lib/rails/test_unit/test_requirer.rb +++ b/railties/lib/rails/test_unit/test_requirer.rb @@ -4,10 +4,13 @@ require "rake/file_list" module Rails class TestRequirer # :nodoc: class << self - def require_files(patterns) + def require_files(patterns, exclude_patterns = []) patterns = expand_patterns(patterns) - Rake::FileList[patterns.compact.presence || "test/**/*_test.rb"].to_a.each do |file| + file_list = Rake::FileList[patterns.compact.presence || "test/**/*_test.rb"] + file_list.exclude(exclude_patterns) + + file_list.to_a.each do |file| require File.expand_path(file) end end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 4c157c1262..33408081f1 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -4,15 +4,15 @@ require "rails/test_unit/minitest_plugin" task default: :test -desc "Runs all tests in test folder" +desc "Runs all tests in test folder except system ones" task :test do $: << "test" - pattern = if ENV.key?("TEST") - ENV["TEST"] + + if ENV.key?("TEST") + Minitest.rake_run([ENV["TEST"]]) else - "test" + Minitest.rake_run(["test"], ["test/system/**/*"]) end - Minitest.rake_run([pattern]) end namespace :test do @@ -47,4 +47,10 @@ namespace :test do $: << "test" Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"]) end + + desc "Run system tests only" + task system: "test:prepare" do + $: << "test" + Minitest.rake_run(["test/system"]) + end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 76de2b4639..2df303750c 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index fd1e1b9662..2d4c7a0f0b 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -12,20 +12,20 @@ require "rails/all" module TestApp class Application < Rails::Application - config.root = File.dirname(__FILE__) + config.root = __dir__ secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" end end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::Stream + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/railties/test/app_loader_test.rb b/railties/test/app_loader_test.rb index 7fd5c72c1c..85f5502b4d 100644 --- a/railties/test/app_loader_test.rb +++ b/railties/test/app_loader_test.rb @@ -17,7 +17,7 @@ class AppLoaderTest < ActiveSupport::TestCase end end - def write(filename, contents=nil) + def write(filename, contents = nil) FileUtils.mkdir_p(File.dirname(filename)) File.write(filename, contents) end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index e84e01a41b..f38cacd6da 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -375,7 +375,7 @@ module ApplicationTests class ::OmgController < ActionController::Base def index flash[:cool_story] = true - render text: "ok" + render plain: "ok" end end @@ -384,7 +384,7 @@ module ApplicationTests get "/assets/demo.js" assert_match "alert()", last_response.body - assert_equal nil, last_response.headers["Set-Cookie"] + assert_nil last_response.headers["Set-Cookie"] end test "files in any assets/ directories are not added to Sprockets" do @@ -475,9 +475,9 @@ module ApplicationTests class ::PostsController < ActionController::Base; end - get "/posts", {}, "HTTPS"=>"off" + get "/posts", {}, "HTTPS" => "off" assert_match('src="http://example.com/assets/application.self.js', last_response.body) - get "/posts", {}, "HTTPS"=>"on" + get "/posts", {}, "HTTPS" => "on" assert_match('src="https://example.com/assets/application.self.js', last_response.body) end diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index 0bbd25db2b..0fb995900f 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -6,17 +6,10 @@ module ApplicationTests def setup build_app - - create_gemfile - update_boot_file_to_use_bundler - @old_gemfile_env = ENV["BUNDLE_GEMFILE"] - ENV["BUNDLE_GEMFILE"] = app_path + "/Gemfile" end def teardown teardown_app - - ENV["BUNDLE_GEMFILE"] = @old_gemfile_env end def test_bin_setup @@ -45,6 +38,10 @@ module ApplicationTests app_file "db/schema.rb", "" output = `bin/setup 2>&1` + + # Ignore line that's only output by Bundler < 1.14 + output.sub!(/^Resolving dependencies\.\.\.\n/, "") + assert_equal(<<-OUTPUT, output) == Installing dependencies == The Gemfile's dependencies are satisfied @@ -59,16 +56,5 @@ Created database 'db/test.sqlite3' OUTPUT end end - - private - def create_gemfile - app_file("Gemfile", "source 'https://rubygems.org'") - app_file("Gemfile", "gem 'rails', path: '#{RAILS_FRAMEWORK_ROOT}'", "a") - app_file("Gemfile", "gem 'sqlite3'", "a") - end - - def update_boot_file_to_use_bundler - app_file("config/boot.rb", "require 'bundler/setup'") - end end end diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index 13fc98f0d6..8360b7bf4b 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -10,7 +10,6 @@ module ApplicationTests def teardown teardown_app - FileUtils.rm_rf(new_app) if File.directory?(new_app) end test "access custom configuration point" do @@ -28,30 +27,16 @@ module ApplicationTests assert_equal 3, x.payment_processing.retries assert_equal true, x.super_debugger assert_equal false, x.hyper_debugger - assert_equal nil, x.nil_debugger + assert_nil x.nil_debugger assert_nil x.i_do_not_exist.zomg - end - test "custom configuration responds to all messages" do - x = Rails.configuration.x + # test that custom configuration responds to all messages assert_equal true, x.respond_to?(: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 private - def new_app - File.expand_path("#{app_path}/../new_app") - end - - def copy_app - FileUtils.cp_r(app_path, new_app) - end - - def app - @app ||= Rails.application - end - def require_environment require "#{app_path}/config/environment" end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 5dceaae96b..06767167a9 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -51,7 +51,7 @@ module ApplicationTests def setup build_app - supress_default_config + suppress_default_config end def teardown @@ -59,7 +59,7 @@ module ApplicationTests FileUtils.rm_rf(new_app) if File.directory?(new_app) end - def supress_default_config + def suppress_default_config FileUtils.mv("#{app_path}/config/environments", "#{app_path}/config/__environments__") end @@ -78,6 +78,18 @@ module ApplicationTests end end + test "Rails.env falls back to development if RAILS_ENV is blank and RACK_ENV is nil" do + with_rails_env("") do + assert_equal "development", Rails.env + end + end + + test "Rails.env falls back to development if RACK_ENV is blank and RAILS_ENV is nil" do + with_rack_env("") do + assert_equal "development", Rails.env + end + end + test "By default logs tags are not set in development" do restore_default_config @@ -369,26 +381,6 @@ module ApplicationTests end end - test "config.serve_static_files is deprecated" do - make_basic_app do |application| - assert_deprecated do - application.config.serve_static_files = true - end - - assert application.config.public_file_server.enabled - end - end - - test "config.static_cache_control is deprecated" do - make_basic_app do |application| - assert_deprecated do - application.config.static_cache_control = "public, max-age=60" - end - - assert_equal application.config.static_cache_control, "public, max-age=60" - end - end - test "Use key_generator when secret_key_base is set" do make_basic_app do |application| application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" @@ -398,7 +390,7 @@ module ApplicationTests class ::OmgController < ActionController::Base def index cookies.signed[:some_key] = "some_value" - render text: cookies[:some_key] + render plain: cookies[:some_key] end end @@ -614,7 +606,7 @@ module ApplicationTests app "development" assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token - assert_equal nil, app.secrets.secret_key_base + assert_nil app.secrets.secret_key_base assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator end @@ -630,10 +622,25 @@ module ApplicationTests app "development" assert_equal "", app.config.secret_token - assert_equal nil, app.secrets.secret_key_base - assert_raise ArgumentError, /\AA secret is required/ do + assert_nil app.secrets.secret_key_base + e = assert_raise ArgumentError do app.key_generator end + assert_match(/\AA secret is required/, e.message) + end + + test "that nested keys are symbolized the same as parents for hashes more than one level deep" do + app_file "config/secrets.yml", <<-YAML + development: + smtp_settings: + address: "smtp.example.com" + user_name: "postmaster@example.com" + password: "697361616320736c6f616e2028656c6f7265737429" + YAML + + app "development" + + assert_equal "697361616320736c6f616e2028656c6f7265737429", app.secrets.smtp_settings[:password] end test "protect from forgery is the default in a new app" do @@ -686,6 +693,66 @@ module ApplicationTests assert_match(/label/, 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 + 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(/data-remote/, last_response.body) + end + + test "form_with generates remote forms 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(/data-remote/, last_response.body) + end + test "default method for update can be changed" do app_file "app/models/post.rb", <<-RUBY class Post @@ -704,7 +771,7 @@ module ApplicationTests end def update - render text: "update" + render plain: "update" end private @@ -978,7 +1045,7 @@ module ApplicationTests class ::OmgController < ActionController::Base def index - render text: env["action_dispatch.show_exceptions"] + render plain: request.env["action_dispatch.show_exceptions"] end end @@ -1008,7 +1075,7 @@ module ApplicationTests app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ApplicationController def create - render text: params[:post].inspect + render plain: params[:post].inspect end end RUBY @@ -1029,7 +1096,7 @@ module ApplicationTests app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ActionController::Base def create - render text: params[:post].permitted? ? "permitted" : "forbidden" + render plain: params[:post].permitted? ? "permitted" : "forbidden" end end RUBY @@ -1043,7 +1110,7 @@ module ApplicationTests app "development" - post "/posts", post: { "title" =>"zomg" } + post "/posts", post: { "title" => "zomg" } assert_equal "permitted", last_response.body end @@ -1051,7 +1118,7 @@ module ApplicationTests app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ActionController::Base def create - render text: params.require(:post).permit(:name) + render plain: params.require(:post).permit(:name) end end RUBY @@ -1067,7 +1134,7 @@ module ApplicationTests assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters - post "/posts", post: { "title" =>"zomg" } + post "/posts", post: { "title" => "zomg" } assert_match "We're sorry, but something went wrong", last_response.body end @@ -1090,7 +1157,7 @@ module ApplicationTests app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ActionController::Base def create - render text: params.permit(post: [:title]) + render plain: params.permit(post: [:title]) end end RUBY @@ -1107,7 +1174,7 @@ module ApplicationTests assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters - post "/posts", post: { "title" =>"zomg" }, format: "json" + post "/posts", post: { "title" => "zomg" }, format: "json" assert_equal 200, last_response.status end @@ -1137,8 +1204,8 @@ module ApplicationTests class ::OmgController < ActionController::Base def index respond_to do |format| - format.html { render text: "HTML" } - format.xml { render text: "XML" } + format.html { render plain: "HTML" } + format.xml { render plain: "XML" } end end end @@ -1178,11 +1245,12 @@ module ApplicationTests end test "config.session_store with :active_record_store without activerecord-session_store gem" do - assert_raise RuntimeError, /activerecord-session_store/ do + e = assert_raise RuntimeError do make_basic_app do |application| application.config.session_store :active_record_store end end + assert_match(/activerecord-session_store/, e.message) end test "default session store initializer does not overwrite the user defined session store even if it is disabled" do @@ -1190,7 +1258,7 @@ module ApplicationTests application.config.session_store :disabled end - assert_equal nil, app.config.session_store + assert_nil app.config.session_store end test "default session store initializer sets session store to cookie store" do @@ -1339,6 +1407,40 @@ module ApplicationTests assert_match "config/database", err.message end + test "loads database.yml using shared keys" do + app_file "config/database.yml", <<-YAML + shared: + username: bobby + adapter: sqlite3 + + development: + database: 'dev_db' + YAML + + app "development" + + ar_config = Rails.application.config.database_configuration + assert_equal "sqlite3", ar_config["development"]["adapter"] + assert_equal "bobby", ar_config["development"]["username"] + assert_equal "dev_db", ar_config["development"]["database"] + end + + test "loads database.yml using shared keys for undefined environments" do + app_file "config/database.yml", <<-YAML + shared: + username: bobby + adapter: sqlite3 + database: 'dev_db' + YAML + + app "development" + + ar_config = Rails.application.config.database_configuration + assert_equal "sqlite3", ar_config["development"]["adapter"] + assert_equal "bobby", ar_config["development"]["username"] + assert_equal "dev_db", ar_config["development"]["database"] + end + test "config.action_mailer.show_previews defaults to true in development" do app "development" @@ -1518,5 +1620,24 @@ module ApplicationTests assert_equal :default, Rails.configuration.debug_exception_response_format end + + test "controller force_ssl declaration can be used even if session_store is disabled" do + make_basic_app do |application| + application.config.session_store :disabled + end + + class ::OmgController < ActionController::Base + force_ssl + + def index + render plain: "Yay! You're on Rails!" + end + end + + get "/" + + assert_equal 301, last_response.status + assert_equal "https://example.org/", last_response.location + end end end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 72f340df34..057d473870 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -136,9 +136,9 @@ class FullStackConsoleTest < ActiveSupport::TestCase assert_output "> " end - def spawn_console + def spawn_console(options) Process.spawn( - "#{app_path}/bin/rails console --sandbox", + "#{app_path}/bin/rails console #{options}", in: @slave, out: @slave, err: @slave ) @@ -146,18 +146,26 @@ class FullStackConsoleTest < ActiveSupport::TestCase end def test_sandbox - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.create" write_prompt "Post.count", "=> 1" @master.puts "quit" - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.transaction { Post.create; raise }" write_prompt "Post.count", "=> 0" @master.puts "quit" end + + def test_environment_option_and_irb_option + spawn_console("test -- --verbose") + + write_prompt "a = 1", "a = 1" + write_prompt "puts Rails.env", "puts Rails.env\r\ntest" + @master.puts "quit" + end end diff --git a/railties/test/application/current_attributes_integration_test.rb b/railties/test/application/current_attributes_integration_test.rb new file mode 100644 index 0000000000..5653ec0be1 --- /dev/null +++ b/railties/test/application/current_attributes_integration_test.rb @@ -0,0 +1,84 @@ +require "isolation/abstract_unit" +require "rack/test" + +class CurrentAttributesIntegrationTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + setup do + build_app + + app_file "app/models/current.rb", <<-RUBY + class Current < ActiveSupport::CurrentAttributes + attribute :customer + + resets { Time.zone = "UTC" } + + def customer=(customer) + super + Time.zone = customer.try(:time_zone) + end + end + RUBY + + app_file "app/models/customer.rb", <<-RUBY + class Customer < Struct.new(:name) + def time_zone + "Copenhagen" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/customers/:action", controller: :customers + end + RUBY + + app_file "app/controllers/customers_controller.rb", <<-RUBY + class CustomersController < ApplicationController + def set_current_customer + Current.customer = Customer.new("david") + render :index + end + + def set_no_customer + render :index + end + end + RUBY + + app_file "app/views/customers/index.html.erb", <<-RUBY + <%= Current.customer.try(:name) || 'noone' %>,<%= Time.zone.name %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "current customer is assigned and cleared" do + get "/customers/set_current_customer" + assert_equal 200, last_response.status + assert_match(/david,Copenhagen/, last_response.body) + + get "/customers/set_no_customer" + assert_equal 200, last_response.status + assert_match(/noone,UTC/, last_response.body) + end + + test "resets after execution" do + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + + Rails.application.executor.wrap do + Current.customer = Customer.new("david") + + assert_equal "david", Current.customer.name + assert_equal "Copenhagen", Time.zone.name + end + + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + end +end diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index 0153f94504..fe581db286 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -169,5 +169,30 @@ module ApplicationTests assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.text.erb")) assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.html.erb")) end + + test "ARGV is mutated as expected" do + require "#{app_path}/config/environment" + Rails::Command.const_set("APP_PATH", "rails/all") + + FileUtils.cd(rails_root) do + ARGV = ["mailer", "notifier", "foo"] + Rails::Command.const_set("ARGV", ARGV) + quietly { Rails::Command.invoke :generate, ARGV } + + assert_equal ["notifier", "foo"], ARGV + end + + Rails::Command.send(:remove_const, "APP_PATH") + end + + test "help does not show hidden namespaces" do + FileUtils.cd(rails_root) do + output = `bin/rails generate --help` + assert_no_match "active_record:migration", output + + output = `bin/rails destroy --help` + assert_no_match "active_record:migration", output + end + end end end diff --git a/railties/test/application/help_test.rb b/railties/test/application/help_test.rb new file mode 100644 index 0000000000..0c3fe8bfa3 --- /dev/null +++ b/railties/test/application/help_test.rb @@ -0,0 +1,23 @@ +require "isolation/abstract_unit" + +class HelpTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = Dir.chdir(app_path) { `bin/rails help` } + assert_match "The most common rails commands are", output + end + + test "short-cut alias works" do + output = Dir.chdir(app_path) { `bin/rails -h` } + assert_match "The most common rails commands are", output + end +end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index b2cd339c1a..eb2c578f91 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -185,7 +185,7 @@ module ApplicationTests test "if there's no config.active_support.bare, all of ActiveSupport is required" do use_frameworks [] require "#{app_path}/config/environment" - assert_nothing_raised { [1,2,3].sample } + assert_nothing_raised { [1, 2, 3].sample } end test "config.active_support.bare does not require all of ActiveSupport" do @@ -219,7 +219,7 @@ module ApplicationTests end require "#{app_path}/config/environment" ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test. - assert ActiveRecord::Base.connection.schema_cache.tables("posts") + assert ActiveRecord::Base.connection.schema_cache.data_sources("posts") end test "expire schema cache dump" do @@ -227,10 +227,8 @@ module ApplicationTests `rails generate model post title:string; bin/rails db:migrate db:schema:cache:dump db:rollback` end - silence_warnings { - require "#{app_path}/config/environment" - assert !ActiveRecord::Base.connection.schema_cache.tables("posts") - } + require "#{app_path}/config/environment" + assert !ActiveRecord::Base.connection.schema_cache.data_sources("posts") end test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do @@ -264,5 +262,13 @@ module ApplicationTests Rails.env = orig_rails_env if orig_rails_env end end + + test "connections checked out during initialization are returned to the pool" do + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.connection + RUBY + require "#{app_path}/config/environment" + assert !ActiveRecord::Base.connection_pool.active_connection? + end end end diff --git a/railties/test/application/initializers/hooks_test.rb b/railties/test/application/initializers/hooks_test.rb index 0309d02730..36926c50ff 100644 --- a/railties/test/application/initializers/hooks_test.rb +++ b/railties/test/application/initializers/hooks_test.rb @@ -31,7 +31,7 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert_equal [1,2,3], $initialization_callbacks + assert_equal [1, 2, 3], $initialization_callbacks end test "hooks block works correctly with eager_load" do @@ -46,7 +46,7 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert_equal [1,2,3,4], $initialization_callbacks + assert_equal [1, 2, 3, 4], $initialization_callbacks end test "after_initialize runs after frameworks have been initialized" do diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 38f96f3ab9..c75a25bc6f 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -357,7 +357,7 @@ class LoadingTest < ActiveSupport::TestCase assert Rails.application.initialized? end - protected + private def setup_ar! ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 790aca2aa4..f5c013dab6 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -485,6 +485,57 @@ module ApplicationTests assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body end + test "mailer preview receives query params" do + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo(name) + @name = name + mail to: "to@example.org" + end + end + RUBY + + html_template "notifier/foo", <<-RUBY + <p>Hello, <%= @name %>!</p> + RUBY + + text_template "notifier/foo", <<-RUBY + Hello, <%= @name %>! + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo(params[:name] || "World") + end + end + RUBY + + app("development") + + 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 + + get "/rails/mailers/notifier/foo?part=text%2Fplain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + 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 + + get "/rails/mailers/notifier/foo?name=Ruby&part=text%2Fhtml" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, Ruby!</p>], last_response.body + end + test "plain text mailer preview with attachment" do image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" @@ -671,6 +722,40 @@ module ApplicationTests assert_match %r[<p>Hello, World!</p>], last_response.body end + test "multipart mailer preview with empty parts" do + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template "notifier/foo", <<-RUBY + RUBY + + html_template "notifier/foo", <<-RUBY + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + end + private def build_app super diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index cd1371359a..93b5263bf7 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -19,7 +19,7 @@ module ApplicationTests class ExpiresController < ApplicationController def expires_header expires_in 10, public: !params[:private] - render text: SecureRandom.hex(16) + render plain: SecureRandom.hex(16) end def expires_etag @@ -32,12 +32,12 @@ module ApplicationTests end def keeps_if_modified_since - render :text => request.headers['If-Modified-Since'] + render plain: request.headers['If-Modified-Since'] end private def render_conditionally(headers) if stale?(headers.merge(public: !params[:private])) - render text: SecureRandom.hex(16) + render plain: SecureRandom.hex(16) end end end @@ -117,12 +117,12 @@ module ApplicationTests assert_equal "miss, store", last_response.headers["X-Rack-Cache"] assert_equal "public", last_response.headers["Cache-Control"] - body = last_response.body etag = last_response.headers["ETag"] - get "/expires/expires_etag", {}, "If-None-Match" => etag + get "/expires/expires_etag", {}, "HTTP_IF_NONE_MATCH" => etag assert_equal "stale, valid, store", last_response.headers["X-Rack-Cache"] - assert_equal body, last_response.body + assert_equal 304, last_response.status + assert_equal "", last_response.body end def test_cache_works_with_etags_private @@ -137,7 +137,7 @@ module ApplicationTests body = last_response.body etag = last_response.headers["ETag"] - get "/expires/expires_etag", { private: true }, "If-None-Match" => etag + get "/expires/expires_etag", { private: true }, "HTTP_IF_NONE_MATCH" => etag assert_equal "miss", last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end @@ -151,12 +151,12 @@ module ApplicationTests assert_equal "miss, store", last_response.headers["X-Rack-Cache"] assert_equal "public", last_response.headers["Cache-Control"] - body = last_response.body last = last_response.headers["Last-Modified"] - get "/expires/expires_last_modified", {}, "If-Modified-Since" => last + get "/expires/expires_last_modified", {}, "HTTP_IF_MODIFIED_SINCE" => last assert_equal "stale, valid, store", last_response.headers["X-Rack-Cache"] - assert_equal body, last_response.body + assert_equal 304, last_response.status + assert_equal "", last_response.body end def test_cache_works_with_last_modified_private @@ -171,7 +171,7 @@ module ApplicationTests body = last_response.body last = last_response.headers["Last-Modified"] - get "/expires/expires_last_modified", { private: true }, "If-Modified-Since" => last + get "/expires/expires_last_modified", { private: true }, "HTTP_IF_MODIFIED_SINCE" => last assert_equal "miss", last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index cbb990f13b..fe07ad3cbe 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -100,6 +100,20 @@ module ApplicationTests end end + test "routing to an nonexistent controller when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resources :articles + end + RUBY + + app.config.action_dispatch.show_exceptions = true + app.config.consider_all_requests_local = true + + get "/articles" + assert_match "<title>Action Controller: Exception caught</title>", last_response.body + end + test "displays diagnostics message when exception raised in template that contains UTF-8" do controller :foo, <<-RUBY class FooController < ActionController::Base diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index a6019a9db4..a14ea589ed 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -70,7 +70,7 @@ module ApplicationTests end def read_session - render text: session[:foo].inspect + render plain: session[:foo].inspect end end RUBY @@ -111,7 +111,7 @@ module ApplicationTests end def read_cookie - render text: cookies[:foo].inspect + render plain: cookies[:foo].inspect end end RUBY @@ -149,19 +149,24 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_encrypted_cookie - render text: cookies.encrypted[:_myapp_session]['foo'] + render plain: cookies.encrypted[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY + add_to_config <<-RUBY + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + require "#{app_path}/config/environment" get "/foo/write_session" @@ -171,9 +176,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "1", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -194,21 +199,24 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_encrypted_cookie - render text: cookies.encrypted[:_myapp_session]['foo'] + render plain: cookies.encrypted[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" @@ -220,9 +228,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "1", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -249,21 +257,88 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_encrypted_cookie - render text: cookies.encrypted[:_myapp_session]['foo'] + render plain: cookies.encrypted[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + + require "#{app_path}/config/environment" + + get "/foo/write_raw_session" + get "/foo/read_session" + assert_equal "1", last_response.body + + get "/foo/write_session" + get "/foo/read_session" + assert_equal "2", last_response.body + + get "/foo/read_encrypted_cookie" + assert_equal "2", last_response.body + + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) + + get "/foo/read_raw_cookie" + assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] + end + + test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + def write_raw_session + # AES-256-CBC with SHA1 HMAC + # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} + cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96" + head :ok + end + + def write_session + session[:foo] = session[:foo] + 1 + head :ok + end + + def read_session + render plain: session[:foo] + end + + def read_encrypted_cookie + render plain: cookies.encrypted[:_myapp_session]['foo'] + end + + def read_raw_cookie + render plain: cookies[:_myapp_session] + end + end + RUBY + + add_to_config <<-RUBY + # Use a static key + secrets.secret_key_base = "known key base" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" @@ -279,9 +354,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "2", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -308,15 +383,15 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_signed_cookie - render text: cookies.signed[:_myapp_session]['foo'] + render plain: cookies.signed[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 45481dc1b6..0a6e5b52e9 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -30,10 +30,10 @@ module ApplicationTests "Rack::Runtime", "Rack::MethodOverride", "ActionDispatch::RequestId", - "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods + "ActionDispatch::RemoteIp", + "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", - "ActionDispatch::RemoteIp", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending", @@ -58,10 +58,10 @@ module ApplicationTests "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "ActionDispatch::RequestId", - "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods + "ActionDispatch::RemoteIp", + "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", - "ActionDispatch::RemoteIp", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "Rack::Head", @@ -70,6 +70,37 @@ module ApplicationTests ], middleware end + test "middleware dependencies" do + boot! + + # The following array-of-arrays describes dependencies between + # middlewares: the first item in each list depends on the + # remaining items (and therefore must occur later in the + # middleware stack). + + dependencies = [ + # Logger needs a fully "corrected" request environment + %w(Rails::Rack::Logger Rack::MethodOverride ActionDispatch::RequestId ActionDispatch::RemoteIp), + + # Serving public/ doesn't invoke user code, so it should skip + # locks etc + %w(ActionDispatch::Executor ActionDispatch::Static), + + # Errors during reload must be reported + %w(ActionDispatch::Reloader ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions), + + # Outright dependencies + %w(ActionDispatch::Static Rack::Sendfile), + %w(ActionDispatch::Flash ActionDispatch::Session::CookieStore), + %w(ActionDispatch::Session::CookieStore ActionDispatch::Cookies), + ] + + require "tsort" + sorted = TSort.tsort((middleware | dependencies.flatten).method(:each), + lambda { |n, &b| dependencies.each { |m, *ds| ds.each(&b) if m == n } }) + assert_equal sorted, middleware + end + test "Rack::Cache is not included by default" do boot! @@ -100,10 +131,10 @@ module ApplicationTests test "ActionDispatch::SSL is configured with options when given" do add_to_config "config.force_ssl = true" - add_to_config "config.ssl_options = { host: 'example.com' }" + add_to_config "config.ssl_options = { redirect: { host: 'example.com' } }" boot! - assert_equal [{ host: "example.com" }], Rails.application.middleware.first.args + assert_equal [{ redirect: { host: "example.com" } }], Rails.application.middleware.first.args end test "removing Active Record omits its middleware" do @@ -227,9 +258,9 @@ module ApplicationTests class ::OmgController < ActionController::Base def index if params[:nothing] - render text: "" + render plain: "" else - render text: "OMG" + render plain: "OMG" end end end @@ -239,23 +270,23 @@ module ApplicationTests get "/" assert_equal 200, last_response.status assert_equal "OMG", last_response.body - assert_equal "text/html; charset=utf-8", last_response.headers["Content-Type"] + assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"] assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"] assert_equal etag, last_response.headers["Etag"] get "/", {}, "HTTP_IF_NONE_MATCH" => etag assert_equal 304, last_response.status assert_equal "", last_response.body - assert_equal nil, last_response.headers["Content-Type"] + assert_nil last_response.headers["Content-Type"] assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"] assert_equal etag, last_response.headers["Etag"] get "/?nothing=true" assert_equal 200, last_response.status assert_equal "", last_response.body - assert_equal "text/html; charset=utf-8", last_response.headers["Content-Type"] + assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"] assert_equal "no-cache", last_response.headers["Cache-Control"] - assert_equal nil, last_response.headers["Etag"] + assert_nil last_response.headers["Etag"] end test "ORIGINAL_FULLPATH is passed to env" do diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index 6c003e9bcc..6e6996a6ba 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -18,6 +18,10 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase class Customer < Struct.new(:name, :id) extend ActiveModel::Naming include ActiveModel::Conversion + + def cache_key + [ name, id ].join("/") + end end RUBY diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 51db634b75..c63f23fa0a 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -1,5 +1,4 @@ require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests @@ -175,7 +174,7 @@ module ApplicationTests `bin/rails generate model book title:string; bin/rails db:migrate db:structure:dump` structure_dump = File.read("db/structure.sql") - assert_match(/CREATE TABLE \"books\"/, structure_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, structure_dump) `bin/rails environment db:drop db:structure:load` assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" @@ -203,7 +202,7 @@ module ApplicationTests stderr_output = capture(:stderr) { `bin/rails db:structure:dump` } assert_empty stderr_output structure_dump = File.read("db/structure.sql") - assert_match(/CREATE TABLE \"posts\"/, structure_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"posts\"/, structure_dump) end end diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb index 7ac37b7700..40488a6aab 100644 --- a/railties/test/application/rake/framework_test.rb +++ b/railties/test/application/rake/framework_test.rb @@ -1,5 +1,4 @@ require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests diff --git a/railties/test/application/rake/log_test.rb b/railties/test/application/rake/log_test.rb new file mode 100644 index 0000000000..fdd3c71fe8 --- /dev/null +++ b/railties/test/application/rake/log_test.rb @@ -0,0 +1,33 @@ +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class LogTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "log:clear clear all environments log files by default" do + Dir.chdir(app_path) do + File.open("config/environments/staging.rb", "w") + + File.write("log/staging.log", "staging") + File.write("log/test.log", "test") + File.write("log/dummy.log", "dummy") + + `rails log:clear` + + assert_equal 0, File.size("log/test.log") + assert_equal 0, File.size("log/staging.log") + assert_equal 5, File.size("log/dummy.log") + end + end + end + end +end diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index 76cb302c62..51dfe2ef98 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -37,6 +37,28 @@ module ApplicationTests end end + test "migration with empty version" do + Dir.chdir(app_path) do + output = `bin/rails db:migrate VERSION= 2>&1` + assert_match(/Empty VERSION provided/, output) + + output = `bin/rails db:migrate:redo VERSION= 2>&1` + assert_match(/Empty VERSION provided/, output) + + output = `bin/rails db:migrate:up VERSION= 2>&1` + assert_match(/VERSION is required/, output) + + output = `bin/rails db:migrate:up 2>&1` + assert_match(/VERSION is required/, output) + + output = `bin/rails db:migrate:down VERSION= 2>&1` + assert_match(/VERSION is required - To go down one migration, use db:rollback/, output) + + output = `bin/rails db:migrate:down 2>&1` + assert_match(/VERSION is required - To go down one migration, use db:rollback/, output) + end + end + test "model and migration generator with change syntax" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; @@ -61,7 +83,7 @@ module ApplicationTests assert_equal "Schema migrations table does not exist yet.\n", output end - test "test migration status" do + test "migration status" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; bin/rails generate migration add_email_to_users email:string; @@ -101,7 +123,7 @@ module ApplicationTests end end - test "test migration status after rollback and redo" do + test "migration status after rollback and redo" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; bin/rails generate migration add_email_to_users email:string; @@ -126,6 +148,62 @@ module ApplicationTests end end + test "migration status after rollback and forward" do + Dir.chdir(app_path) do + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate` + + output = `bin/rails db:migrate:status` + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + `bin/rails db:rollback STEP=2` + output = `bin/rails db:migrate:status` + + assert_match(/down\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + + `bin/rails db:forward STEP=2` + output = `bin/rails db:migrate:status` + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + end + end + + test "raise error on any move when current migration does not exist" do + Dir.chdir(app_path) do + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate + rm db/migrate/*email*.rb` + + output = `bin/rails db:migrate:status` + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + + output = `bin/rails db:rollback 2>&1` + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number\s\d{14}\./, output) + + output = `bin/rails db:migrate:status` + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + + output = `bin/rails db:forward 2>&1` + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number\s\d{14}\./, output) + + output = `bin/rails db:migrate:status` + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + end + end + test "migration status after rollback and redo without timestamps" do add_to_config("config.active_record.timestamped_migrations = false") @@ -208,7 +286,7 @@ module ApplicationTests end end - test "test migration status migrated file is deleted" do + test "migration status migrated file is deleted" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; bin/rails generate migration add_email_to_users email:string; @@ -216,7 +294,6 @@ module ApplicationTests rm db/migrate/*email*.rb` output = `bin/rails db:migrate:status` - File.write("test.txt", output) assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 5fd5507453..1b64a0a1ca 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -132,43 +132,21 @@ module ApplicationTests assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end - def test_rails_routes_with_controller_environment + def test_singular_resource_output_in_rake_routes app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do - get '/cart', to: 'cart#show' - get '/basketball', to: 'basketball#index' + resource :post end RUBY - output = Dir.chdir(app_path) { `bin/rails routes CONTROLLER=cart` } - assert_equal ["Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.", - "Please use `bin/rails routes -c controller_name` instead.", - "Prefix Verb URI Pattern Controller#Action", - " cart GET /cart(.:format) cart#show\n"].join("\n"), output - - output = Dir.chdir(app_path) { `bin/rails routes -c cart` } - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output - end - - def test_rails_routes_with_namespaced_controller_environment - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - namespace :admin do - resource :post - end - end - RUBY - expected_output = [" Prefix Verb URI Pattern Controller#Action", - " admin_post POST /admin/post(.:format) admin/posts#create", - " new_admin_post GET /admin/post/new(.:format) admin/posts#new", - "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", - " GET /admin/post(.:format) admin/posts#show", - " PATCH /admin/post(.:format) admin/posts#update", - " PUT /admin/post(.:format) admin/posts#update", - " DELETE /admin/post(.:format) admin/posts#destroy\n"].join("\n") - - output = Dir.chdir(app_path) { `bin/rails routes -c Admin::PostController` } - assert_equal expected_output, output + expected_output = [" Prefix Verb URI Pattern Controller#Action", + " new_post GET /post/new(.:format) posts#new", + "edit_post GET /post/edit(.:format) posts#edit", + " post GET /post(.:format) posts#show", + " PATCH /post(.:format) posts#update", + " PUT /post(.:format) posts#update", + " DELETE /post(.:format) posts#destroy", + " POST /post(.:format) posts#create\n"].join("\n") output = Dir.chdir(app_path) { `bin/rails routes -c PostController` } assert_equal expected_output, output @@ -212,6 +190,30 @@ module ApplicationTests assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end + def test_rails_routes_with_namespaced_controller_search_key + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + namespace :admin do + resource :post + end + end + RUBY + expected_output = [" Prefix Verb URI Pattern Controller#Action", + " new_admin_post GET /admin/post/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", + " admin_post GET /admin/post(.:format) admin/posts#show", + " PATCH /admin/post(.:format) admin/posts#update", + " PUT /admin/post(.:format) admin/posts#update", + " DELETE /admin/post(.:format) admin/posts#destroy", + " POST /admin/post(.:format) admin/posts#create\n"].join("\n") + + output = Dir.chdir(app_path) { `bin/rails routes -c Admin::PostController` } + assert_equal expected_output, output + + output = Dir.chdir(app_path) { `bin/rails routes -c PostController` } + assert_equal expected_output, output + end + def test_rails_routes_displays_message_when_no_routes_are_defined app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do @@ -321,16 +323,6 @@ module ApplicationTests assert_no_match(/Errors running/, output) end - def test_db_test_clone_when_using_sql_format - add_to_config "config.active_record.schema_format = :sql" - output = Dir.chdir(app_path) do - `bin/rails generate scaffold user username:string; - bin/rails db:migrate; - bin/rails db:test:clone 2>&1 --trace` - end - assert_match(/Execute db:test:clone_structure/, output) - end - def test_db_test_prepare_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do @@ -367,14 +359,14 @@ module ApplicationTests bin/rails generate model product name:string; bin/rails db:migrate db:schema:cache:dump` end - assert File.exist?(File.join(app_path, "db", "schema_cache.dump")) + assert File.exist?(File.join(app_path, "db", "schema_cache.yml")) end def test_rake_clear_schema_cache Dir.chdir(app_path) do `bin/rails db:schema:cache:dump db:schema:cache:clear` end - assert !File.exist?(File.join(app_path, "db", "schema_cache.dump")) + assert !File.exist?(File.join(app_path, "db", "schema_cache.yml")) end def test_copy_templates diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index c86759de5a..6742da20cc 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -65,7 +65,7 @@ module ApplicationTests controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -156,7 +156,7 @@ module ApplicationTests controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: my_blog_path + render plain: my_blog_path end end RUBY @@ -176,7 +176,7 @@ module ApplicationTests controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -184,7 +184,7 @@ module ApplicationTests controller :bar, <<-RUBY class BarController < ActionController::Base def index - render text: "bar" + render plain: "bar" end end RUBY @@ -206,7 +206,7 @@ module ApplicationTests controller "foo", <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -215,7 +215,7 @@ module ApplicationTests module Admin class FooController < ApplicationController def index - render text: "admin::foo" + render plain: "admin::foo" end end end @@ -263,16 +263,42 @@ module ApplicationTests assert_equal "WIN", last_response.body end - { "development" => "baz", "production" => "bar" }.each do |mode, expected| + { + "development" => ["baz", "http://www.apple.com", "/dashboard"], + "production" => ["bar", "http://www.microsoft.com", "/profile"] + }.each do |mode, (expected_action, expected_url, expected_mapping)| test "reloads routes when configuration is changed in #{mode}" do controller :foo, <<-RUBY class FooController < ApplicationController def bar - render text: "bar" + render plain: "bar" end def baz - render text: "baz" + render plain: "baz" + end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) + end + end + RUBY + + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false end end RUBY @@ -280,6 +306,11 @@ module ApplicationTests app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#bar' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.microsoft.com" } + resolve("User") { "/profile" } end RUBY @@ -288,16 +319,33 @@ module ApplicationTests get "/foo" assert_equal "bar", last_response.body + get "/custom" + assert_equal "http://www.microsoft.com", last_response.body + + get "/mapping" + assert_equal "/profile", last_response.body + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#baz' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.apple.com" } + resolve("User") { "/dashboard" } end RUBY sleep 0.1 get "/foo" - assert_equal expected, last_response.body + assert_equal expected_action, last_response.body + + get "/custom" + assert_equal expected_url, last_response.body + + get "/mapping" + assert_equal expected_mapping, last_response.body end end @@ -332,7 +380,7 @@ module ApplicationTests controller :foo, <<-RUBY class FooController < ApplicationController def index - render :text => "foo" + render plain: "foo" end end RUBY @@ -356,7 +404,15 @@ module ApplicationTests controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" + end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) end end RUBY @@ -364,7 +420,22 @@ module ApplicationTests controller :bar, <<-RUBY class BarController < ApplicationController def index - render text: "bar" + render plain: "bar" + end + end + RUBY + + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false end end RUBY @@ -389,6 +460,12 @@ module ApplicationTests Rails.application.routes.draw do get 'foo', to: 'foo#index' get 'bar', to: 'bar#index' + + get 'custom', to: 'foo#custom' + direct(:custom) { 'http://www.apple.com' } + + get 'mapping', to: 'foo#mapping' + resolve('User') { '/profile' } end RUBY @@ -402,6 +479,14 @@ module ApplicationTests assert_equal "bar", last_response.body assert_equal "/bar", Rails.application.routes.url_helpers.bar_path + get "/custom" + assert_equal "http://www.apple.com", last_response.body + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + + get "/mapping" + assert_equal "/profile", last_response.body + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' @@ -419,6 +504,18 @@ module ApplicationTests assert_raises NoMethodError do assert_equal "/bar", Rails.application.routes.url_helpers.bar_path end + + get "/custom" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + end + + get "/mapping" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + end end test "named routes are cleared when reloading" do @@ -427,7 +524,7 @@ module ApplicationTests controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -435,7 +532,22 @@ module ApplicationTests controller :bar, <<-RUBY class BarController < ApplicationController def index - render text: "bar" + render plain: "bar" + end + end + RUBY + + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false end end RUBY @@ -443,16 +555,23 @@ module ApplicationTests app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/foo', to: 'foo#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:microsoft) { 'http://www.microsoft.com' } + resolve('User') { '/profile' } end RUBY get "/en/foo" assert_equal "foo", last_response.body assert_equal "/en/foo", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/bar', to: 'bar#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:apple) { 'http://www.apple.com' } end RUBY @@ -464,6 +583,12 @@ module ApplicationTests get "/en/bar" assert_equal "bar", last_response.body assert_equal "/en/bar", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.apple_url + assert_equal "/users", Rails.application.routes.url_helpers.polymorphic_path(User.new) + + assert_raises NoMethodError do + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + end end test "resource routing with irregular inflection" do @@ -482,7 +607,7 @@ module ApplicationTests controller "yazilar", <<-RUBY class YazilarController < ApplicationController def index - render text: 'yazilar#index' + render plain: 'yazilar#index' end end RUBY @@ -493,5 +618,63 @@ module ApplicationTests get "/yazilar" assert_equal 200, last_response.status end + + test "reloading routes removes methods and doesn't undefine them" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + app_file "app/models/url_helpers.rb", <<-RUBY + module UrlHelpers + def foo_path + "/foo" + end + end + RUBY + + app_file "app/models/context.rb", <<-RUBY + class Context + include UrlHelpers + include Rails.application.routes.url_helpers + end + RUBY + + controller "url", <<-RUBY + class UrlController < ApplicationController + def index + context = Context.new + render plain: context.foo_path + end + end + RUBY + + get "/url" + assert_equal "/foo", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + get '/bar', to: 'foo#index', as: 'foo' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/bar", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/foo", last_response.body + end end end diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index 77e7a2cca5..0c45bc398a 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -35,6 +35,14 @@ module ApplicationTests assert_match "42", Dir.chdir(app_path) { `bin/rails runner "puts User.count"` } end + def test_should_set_argv_when_running_code + output = Dir.chdir(app_path) { + # Both long and short args, at start and end of ARGV + `bin/rails runner "puts ARGV.join(',')" --foo a1 -b a2 a3 --moo` + } + assert_equal "--foo,a1,-b,a2,a3,--moo", output.chomp + end + def test_should_run_file app_file "bin/count_users.rb", <<-SCRIPT puts User.count @@ -43,6 +51,15 @@ module ApplicationTests assert_match "42", Dir.chdir(app_path) { `bin/rails runner "bin/count_users.rb"` } end + def test_no_minitest_loaded_in_production_mode + app_file "bin/print_features.rb", <<-SCRIPT + p $LOADED_FEATURES.grep(/minitest/) + SCRIPT + assert_match "[]", Dir.chdir(app_path) { + `RAILS_ENV=production bin/rails runner "bin/print_features.rb"` + } + end + def test_should_set_dollar_0_to_file app_file "bin/dollar0.rb", <<-SCRIPT puts $0 @@ -59,6 +76,14 @@ module ApplicationTests assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bin/rails runner "bin/program_name.rb"` } end + def test_passes_extra_args_to_file + app_file "bin/program_name.rb", <<-SCRIPT + p ARGV + SCRIPT + + assert_match %w( a b ).to_s, Dir.chdir(app_path) { `bin/rails runner "bin/program_name.rb" a b` } + end + def test_with_hook add_to_config <<-RUBY runner do |app| diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index b442891769..8e0712fca2 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -15,6 +15,16 @@ module ApplicationTests teardown_app end + def test_run_via_backwardscompatibility + require "rails/test_unit/minitest_plugin" + + assert_nothing_raised do + Minitest.run_via[:ruby] = true + end + + assert_predicate Minitest.run_via, :ruby? + end + def test_run_single_file create_test_file :models, "foo" create_test_file :models, "bar" @@ -60,16 +70,18 @@ module ApplicationTests end def test_run_units - skip "we no longer have the concept of unit tests. Just different directories..." create_test_file :models, "foo" create_test_file :helpers, "bar_helper" create_test_file :unit, "baz_unit" create_test_file :controllers, "foobar_controller" - run_test_units_command.tap do |output| - assert_match "FooTest", output - assert_match "BarHelperTest", output - assert_match "BazUnitTest", output - assert_match "3 runs, 3 assertions, 0 failures", output + + Dir.chdir(app_path) do + `bin/rails test:units`.tap do |output| + assert_match "FooTest", output + assert_match "BarHelperTest", output + assert_match "BazUnitTest", output + assert_match "3 runs, 3 assertions, 0 failures", output + end end end @@ -107,16 +119,18 @@ module ApplicationTests end def test_run_functionals - skip "we no longer have the concept of functional tests. Just different directories..." create_test_file :mailers, "foo_mailer" create_test_file :controllers, "bar_controller" create_test_file :functional, "baz_functional" create_test_file :models, "foo" - run_test_functionals_command.tap do |output| - assert_match "FooMailerTest", output - assert_match "BarControllerTest", output - assert_match "BazFunctionalTest", output - assert_match "3 runs, 3 assertions, 0 failures", output + + Dir.chdir(app_path) do + `bin/rails test:functionals`.tap do |output| + assert_match "FooMailerTest", output + assert_match "BarControllerTest", output + assert_match "BazFunctionalTest", output + assert_match "3 runs, 3 assertions, 0 failures", output + end end end @@ -309,7 +323,7 @@ module ApplicationTests assert true end - test "test line filter does not run this" do + test "line filter does not run this" do assert true end end @@ -455,7 +469,7 @@ module ApplicationTests def test_run_app_without_rails_loaded # Simulate a real Rails app boot. app_file "config/boot.rb", <<-RUBY - ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. RUBY @@ -495,7 +509,7 @@ module ApplicationTests create_test_file :models, "post", pass: false # This specifically verifies TEST for backwards compatibility with rake test # as bin/rails test already supports running tests from a single file more cleanly. - output = Dir.chdir(app_path) { `bin/rake test TEST=test/models/post_test.rb` } + output = Dir.chdir(app_path) { `bin/rake test TEST=test/models/post_test.rb` } assert_match "PostTest", output, "passing TEST= should run selected test" assert_no_match "AccountTest", output, "passing TEST= should only run selected test" @@ -504,7 +518,7 @@ module ApplicationTests def test_pass_rake_options create_test_file :models, "account" - output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile --trace=stdout test` } + output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile --trace=stdout test` } assert_match "1 runs, 1 assertions", output assert_match "Execute test", output @@ -512,30 +526,130 @@ module ApplicationTests def test_rails_db_create_all_restores_db_connection create_test_file :models, "account" - output = Dir.chdir(app_path) { `bin/rails db:create:all db:migrate && echo ".tables" | rails dbconsole` } + output = Dir.chdir(app_path) { `bin/rails db:create:all db:migrate && echo ".tables" | rails dbconsole` } assert_match "ar_internal_metadata", output, "tables should be dumped" end def test_rails_db_create_all_restores_db_connection_after_drop create_test_file :models, "account" Dir.chdir(app_path) { `bin/rails db:create:all` } # create all to avoid warnings - output = Dir.chdir(app_path) { `bin/rails db:drop:all db:create:all db:migrate && echo ".tables" | rails dbconsole` } + output = Dir.chdir(app_path) { `bin/rails db:drop:all db:create:all db:migrate && echo ".tables" | rails dbconsole` } assert_match "ar_internal_metadata", output, "tables should be dumped" end def test_rake_passes_TESTOPTS_to_minitest create_test_file :models, "account" - output = Dir.chdir(app_path) { `bin/rake test TESTOPTS=-v` } + output = Dir.chdir(app_path) { `bin/rake test TESTOPTS=-v` } assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" 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'` } + output = Dir.chdir(app_path) { `bin/rake test TESTOPTS='-v --seed=1234'` } assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" assert_match "seed=1234", output, "passing TEST= should run selected test" end + def test_rake_runs_multiple_test_tasks + create_test_file :models, "account" + create_test_file :controllers, "accounts_controller" + output = Dir.chdir(app_path) { `bin/rake test:models test:controllers TESTOPTS='-v'` } + assert_match "AccountTest#test_truth", output + assert_match "AccountsControllerTest#test_truth", output + end + + def test_rake_db_and_test_tasks_parses_args_correctly + create_test_file :models, "account" + output = Dir.chdir(app_path) { `bin/rake db:migrate test:models TESTOPTS='-v' && echo ".tables" | rails dbconsole` } + assert_match "AccountTest#test_truth", output + assert_match "ar_internal_metadata", output + end + + def test_warnings_option + app_file "test/models/warnings_test.rb", <<-RUBY + require 'test_helper' + def test_warnings + a = 1 + end + RUBY + assert_match(/warning: assigned but unused variable/, + capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") }) + end + + def test_reset_sessions_before_rollback_on_system_tests + app_file "test/system/reset_session_before_rollback_test.rb", <<-RUBY + require "application_system_test_case" + + class ResetSessionBeforeRollbackTest < ApplicationSystemTestCase + def teardown_fixtures + puts "rollback" + super + end + + Capybara.singleton_class.prepend(Module.new do + def reset_sessions! + puts "reset sessions" + super + end + end) + + test "dummy" do + end + end + RUBY + + run_test_command("test/system/reset_session_before_rollback_test.rb").tap do |output| + assert_match "reset sessions\nrollback", output + assert_match "1 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output + end + end + + def test_system_tests_are_not_run_with_the_default_test_command + app_file "test/system/dummy_test.rb", <<-RUBY + require "application_system_test_case" + + class DummyTest < ApplicationSystemTestCase + test "something" do + assert true + end + end + RUBY + + run_test_command("").tap do |output| + assert_match "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output + end + end + + def test_system_tests_are_not_run_through_rake_test + app_file "test/system/dummy_test.rb", <<-RUBY + require "application_system_test_case" + + class DummyTest < ApplicationSystemTestCase + test "something" do + assert true + end + end + RUBY + + output = Dir.chdir(app_path) { `bin/rake test` } + assert_match "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output + end + + def test_system_tests_are_run_through_rake_test_when_given_in_TEST + app_file "test/system/dummy_test.rb", <<-RUBY + require "application_system_test_case" + + class DummyTest < ApplicationSystemTestCase + test "something" do + assert true + end + end + RUBY + + output = Dir.chdir(app_path) { `bin/rake test TEST=test/system/dummy_test.rb` } + assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", output + end + private def run_test_command(arguments = "test/unit/test_test.rb") Dir.chdir(app_path) { `bin/rails t #{arguments}` } diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb index ced6c6b0a6..37f129475c 100644 --- a/railties/test/application/url_generation_test.rb +++ b/railties/test/application/url_generation_test.rb @@ -27,7 +27,7 @@ module ApplicationTests class ::OmgController < ::ApplicationController def index - render text: omg_path + render plain: omg_path end end diff --git a/railties/test/application/version_test.rb b/railties/test/application/version_test.rb new file mode 100644 index 0000000000..6b419ae7ae --- /dev/null +++ b/railties/test/application/version_test.rb @@ -0,0 +1,24 @@ +require "isolation/abstract_unit" +require "rails/gem_version" + +class VersionTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = Dir.chdir(app_path) { `bin/rails version` } + assert_equal "Rails #{Rails.gem_version}\n", output + end + + test "short-cut alias works" do + output = Dir.chdir(app_path) { `bin/rails -v` } + assert_equal "Rails #{Rails.gem_version}\n", output + end +end diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb index 17201d6f77..f71e56f323 100644 --- a/railties/test/backtrace_cleaner_test.rb +++ b/railties/test/backtrace_cleaner_test.rb @@ -15,7 +15,7 @@ class BacktraceCleanerTest < ActiveSupport::TestCase test "should format installed gems not in Gem.default_dir correctly" do target_dir = Gem.path.detect { |p| p != Gem.default_dir } # skip this test if default_dir is the only directory on Gem.path - if @target_dir + if target_dir backtrace = [ "#{target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] result = @cleaner.clean(backtrace, :all) assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb index 2dba3c6e96..25a8a40d27 100644 --- a/railties/test/code_statistics_calculator_test.rb +++ b/railties/test/code_statistics_calculator_test.rb @@ -24,7 +24,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase end end - test "count number of methods in MiniTest file" do + test "count number of methods in Minitest file" do code = <<-RUBY class FooTest < ActionController::TestCase test 'expectation' do @@ -52,7 +52,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 3, @code_statistics_calculator.classes assert_equal 4, @code_statistics_calculator.methods - code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5) + code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5) @code_statistics_calculator.add(code_statistics_calculator_2) assert_equal 3, @code_statistics_calculator.lines @@ -317,7 +317,7 @@ class Animal private def temp_file(name, content) - dir = File.expand_path "../fixtures/tmp", __FILE__ + dir = File.expand_path "fixtures/tmp", __dir__ path = "#{dir}/#{name}" FileUtils.mkdir_p dir diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb index 965b6eeb79..e6e3943117 100644 --- a/railties/test/code_statistics_test.rb +++ b/railties/test/code_statistics_test.rb @@ -3,7 +3,7 @@ require "rails/code_statistics" class CodeStatisticsTest < ActiveSupport::TestCase def setup - @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), "fixtures", "tmp")) + @tmp_path = File.expand_path("fixtures/tmp", __dir__) @dir_js = File.join(@tmp_path, "lib.js") FileUtils.mkdir_p(@dir_js) end diff --git a/railties/test/command/base_test.rb b/railties/test/command/base_test.rb new file mode 100644 index 0000000000..ebfc4d0ba9 --- /dev/null +++ b/railties/test/command/base_test.rb @@ -0,0 +1,11 @@ +require "abstract_unit" +require "rails/command" +require "rails/commands/generate/generate_command" +require "rails/commands/secrets/secrets_command" + +class Rails::Command::BaseTest < ActiveSupport::TestCase + test "printing commands" do + assert_equal %w(generate), Rails::Command::GenerateCommand.printing_commands + assert_equal %w(secrets:setup secrets:edit), Rails::Command::SecretsCommand.printing_commands + end +end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 2ddb269eae..0f8c5dbb79 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -15,15 +15,15 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_config_with_db_config_only config_sample = { - "test"=> { - "adapter"=> "sqlite3", - "host"=> "localhost", - "port"=> "9000", - "database"=> "foo_test", - "user"=> "foo", - "password"=> "bar", - "pool"=> "5", - "timeout"=> "3000" + "test" => { + "adapter" => "sqlite3", + "host" => "localhost", + "port" => "9000", + "database" => "foo_test", + "user" => "foo", + "password" => "bar", + "pool" => "5", + "timeout" => "3000" } } app_db_config(config_sample) do diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb new file mode 100644 index 0000000000..be610f3b47 --- /dev/null +++ b/railties/test/commands/secrets_test.rb @@ -0,0 +1,34 @@ +require "isolation/abstract_unit" +require "rails/command" +require "rails/commands/secrets/secrets_command" + +class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "edit without editor gives hint" do + assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "") + 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) + + # Run twice to ensure encrypted secrets can be reread after first edit pass. + 2.times do + assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_edit_command) + end + end + + private + def run_edit_command(editor: "cat") + Dir.chdir(app_path) { `EDITOR="#{editor}" bin/rails secrets:edit` } + end +end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 391886bf33..722323efdc 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -7,31 +7,35 @@ class Rails::ServerTest < ActiveSupport::TestCase include EnvHelpers def test_environment_with_server_option - args = ["thin", "-e", "production"] - options = Rails::Server::Options.new.parse!(args) + args = ["thin", "-e", "production"] + options = parse_arguments(args) assert_equal "production", options[:environment] assert_equal "thin", options[:server] end def test_environment_without_server_option - args = ["-e", "production"] - options = Rails::Server::Options.new.parse!(args) + args = ["-e", "production"] + options = parse_arguments(args) assert_equal "production", options[:environment] assert_nil options[:server] end def test_server_option_without_environment - args = ["thin"] - options = Rails::Server::Options.new.parse!(args) - assert_nil options[:environment] - assert_equal "thin", options[:server] + args = ["thin"] + with_rack_env nil do + with_rails_env nil do + options = parse_arguments(args) + assert_equal "development", options[:environment] + assert_equal "thin", options[:server] + end + end end def test_environment_with_rails_env with_rack_env nil do with_rails_env "production" do - server = Rails::Server.new - assert_equal "production", server.options[:environment] + options = parse_arguments + assert_equal "production", options[:environment] end end end @@ -39,40 +43,39 @@ class Rails::ServerTest < ActiveSupport::TestCase def test_environment_with_rack_env with_rails_env nil do with_rack_env "production" do - server = Rails::Server.new - assert_equal "production", server.options[:environment] + options = parse_arguments + assert_equal "production", options[:environment] end end end def test_environment_with_port switch_env "PORT", "1234" do - server = Rails::Server.new - assert_equal 1234, server.options[:Port] + options = parse_arguments + assert_equal 1234, options[:Port] end end def test_environment_with_host switch_env "HOST", "1.2.3.4" do - server = Rails::Server.new - assert_equal "1.2.3.4", server.options[:Host] + options = parse_arguments + assert_equal "1.2.3.4", options[:Host] end end def test_caching_without_option args = [] - options = Rails::Server::Options.new.parse!(args) - merged_options = Rails::Server.new.default_options.merge(options) - assert_equal nil, merged_options[:caching] + options = parse_arguments(args) + assert_nil options[:caching] end def test_caching_with_option args = ["--dev-caching"] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:caching] args = ["--no-dev-caching"] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:caching] end @@ -80,44 +83,96 @@ class Rails::ServerTest < ActiveSupport::TestCase with_rack_env nil do with_rails_env nil do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] args = ["-e", "development"] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] args = ["-e", "production"] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:log_stdout] with_rack_env "development" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] end with_rack_env "production" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:log_stdout] end with_rails_env "development" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] end with_rails_env "production" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:log_stdout] end end end end + def test_host + with_rails_env "development" do + options = parse_arguments([]) + assert_equal "localhost", options[:Host] + end + + with_rails_env "production" do + options = parse_arguments([]) + assert_equal "0.0.0.0", options[:Host] + end + + with_rails_env "development" do + args = ["-b", "127.0.0.1"] + options = parse_arguments(args) + assert_equal "127.0.0.1", options[:Host] + end + end + + def test_argument_precedence_over_environment_variable + switch_env "PORT", "1234" do + args = ["-p", "5678"] + options = parse_arguments(args) + assert_equal 5678, options[:Port] + end + + switch_env "PORT", "1234" do + args = ["-p", "3000"] + options = parse_arguments(args) + assert_equal 3000, options[:Port] + end + + switch_env "HOST", "1.2.3.4" do + args = ["-b", "127.0.0.1"] + options = parse_arguments(args) + assert_equal "127.0.0.1", options[:Host] + end + end + + def test_records_user_supplied_options + server_options = parse_arguments(["-p", 3001]) + assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port", 3001]) + assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["-p3001", "-C", "--binding", "127.0.0.1"]) + assert_equal [:Port, :Host, :caching], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port=3001"]) + assert_equal [:Port], server_options[:user_supplied_options] + end + def test_default_options server = Rails::Server.new old_default_options = server.default_options @@ -129,15 +184,19 @@ class Rails::ServerTest < ActiveSupport::TestCase def test_restart_command_contains_customized_options original_args = ARGV.dup - args = ["-p", "4567"] + args = %w(-p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C) ARGV.replace args - options = Rails::Server::Options.new.parse! args - server = Rails::Server.new options - expected = "bin/rails server -p 4567" + options = parse_arguments(args) + expected = "bin/rails server -p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C" - assert_equal expected, server.default_options[:restart_cmd] + assert_equal expected, options[:restart_cmd] ensure ARGV.replace original_args end + + private + def parse_arguments(args = []) + Rails::Command::ServerCommand.new([], args).server_options + end end diff --git a/railties/test/engine/commands_tasks_test.rb b/railties/test/engine/commands_tasks_test.rb deleted file mode 100644 index 817175b9ef..0000000000 --- a/railties/test/engine/commands_tasks_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "abstract_unit" - -class Rails::Engine::CommandsTasksTest < ActiveSupport::TestCase - def setup - @destination_root = Dir.mktmpdir("bukkits") - Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } - end - - def teardown - FileUtils.rm_rf(@destination_root) - end - - def test_help_command_work_inside_engine - output = capture(:stderr) do - Dir.chdir(plugin_path) { `bin/rails --help` } - end - assert_no_match "NameError", output - end - - private - def plugin_path - "#{@destination_root}/bukkits" - end -end diff --git a/railties/test/engine/commands_test.rb b/railties/test/engine/commands_test.rb new file mode 100644 index 0000000000..b1c937143f --- /dev/null +++ b/railties/test/engine/commands_test.rb @@ -0,0 +1,96 @@ +require "abstract_unit" +begin + require "pty" +rescue LoadError +end + +class Rails::Engine::CommandsTest < ActiveSupport::TestCase + def setup + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_help_command_work_inside_engine + output = capture(:stderr) do + Dir.chdir(plugin_path) { `bin/rails --help` } + end + assert_no_match "NameError", output + end + + def test_runner_command_work_inside_engine + output = capture(:stdout) do + Dir.chdir(plugin_path) { system("bin/rails runner 'puts Rails.env'") } + end + + assert_equal "test", output.strip + end + + def test_console_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + master, slave = PTY.open + spawn_command("console", slave) + assert_output(">", master) + ensure + master.puts "quit" + end + + def test_dbconsole_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + master, slave = PTY.open + spawn_command("dbconsole", slave) + assert_output("sqlite>", master) + ensure + master.puts ".exit" + end + + def test_server_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + master, slave = PTY.open + pid = spawn_command("server", slave) + assert_output("Listening on", master) + ensure + kill(pid) + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def assert_output(expected, io, timeout = 10) + timeout = Time.now + timeout + + output = "" + until output.include?(expected) || Time.now > timeout + if IO.select([io], [], [], 0.1) + output << io.read(1) + end + end + + assert_includes output, expected, "#{expected.inspect} expected, but got:\n\n#{output}" + end + + def spawn_command(command, fd) + Process.spawn( + "#{plugin_path}/bin/rails #{command}", + in: fd, out: fd, err: fd + ) + end + + def available_pty? + defined?(PTY) && PTY.respond_to?(:open) + end + + def kill(pid) + Process.kill("TERM", pid) + Process.wait(pid) + rescue Errno::ESRCH + end +end diff --git a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb index 21b0ff6c28..701515440a 100644 --- a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb +++ b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb @@ -1,5 +1,5 @@ require "rails/generators" class UsageTemplateGenerator < Rails::Generators::Base - source_root File.expand_path("templates", File.dirname(__FILE__)) + source_root File.expand_path("templates", __dir__) end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 0a26897a4d..360e8e97d7 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -369,7 +369,7 @@ F assert_equal("", action(:log, :yes, "YES")) end - protected + private def action(*args, &block) capture(:stdout) { generator.send(*args, &block) } diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index bbb814ef4e..a19e0f0dd8 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -35,7 +35,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_no_match(/gem 'coffee-rails'/, content) - assert_no_match(/gem 'jquery-rails'/, content) assert_no_match(/gem 'sass-rails'/, content) assert_no_match(/gem 'web-console'/, content) assert_match(/# gem 'jbuilder'/, content) @@ -62,13 +61,25 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase end end - def test_generator_skips_per_form_csrf_token_and_origin_check_configs_for_api_apps + def test_app_update_does_not_generate_unnecessary_config_files run_generator - assert_file "config/initializers/new_framework_defaults.rb" do |initializer_content| - assert_no_match(/per_form_csrf_tokens/, initializer_content) - assert_no_match(/forgery_protection_origin_check/, initializer_content) - end + generator = Rails::Generators::AppGenerator.new ["rails"], + { api: true, update: true }, destination_root: destination_root, shell: @shell + quietly { generator.send(:update_config_files) } + + assert_no_file "config/initializers/cookies_serializer.rb" + assert_no_file "config/initializers/assets.rb" + end + + def test_app_update_does_not_generate_unnecessary_bin_files + run_generator + + generator = Rails::Generators::AppGenerator.new ["rails"], + { api: true, update: true }, destination_root: destination_root, shell: @shell + quietly { generator.send(:update_bin_files) } + + assert_no_file "bin/yarn" end private @@ -106,10 +117,10 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase %w(app/assets app/helpers app/views/layouts/application.html.erb + bin/yarn config/initializers/assets.rb config/initializers/cookies_serializer.rb lib/assets - vendor/assets test/helpers tmp/cache/assets public/404.html @@ -117,6 +128,8 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase public/500.html public/apple-touch-icon-precomposed.png public/apple-touch-icon.png - public/favicon.ico) + public/favicon.icon + package.json + ) end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 8f7fa1155f..8a8c9a35ce 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -42,10 +42,8 @@ DEFAULT_APP_FILES = %w( test/helpers test/mailers test/integration + test/system vendor - vendor/assets - vendor/assets/stylesheets - vendor/assets/javascripts tmp tmp/cache tmp/cache/assets @@ -135,7 +133,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_rails_update_generates_correct_session_key + def test_app_update_generates_correct_session_key app_root = File.join(destination_root, "myapp") run_generator [app_root] @@ -158,67 +156,65 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "config/initializers/cors.rb" end - def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured - app_root = File.join(destination_root, "myapp") + def test_new_application_doesnt_need_defaults + assert_no_file "config/initializers/new_framework_defaults_5_2.rb" + end + + def test_new_application_load_defaults + app_root = File.join(destination_root, "myfirstapp") run_generator [app_root] + output = nil - stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + Dir.chdir(app_root) do + output = `./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"` end + + assert_equal "false\n", output end - def test_rails_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured + def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured app_root = File.join(destination_root, "myapp") run_generator [app_root] - FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") - stub_rails_application(app_root) do generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", - /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) end end - def test_rails_update_dont_set_file_watcher + def test_app_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured app_root = File.join(destination_root, "myapp") run_generator [app_root] + FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") + stub_rails_application(app_root) do generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/environments/development.rb" do |content| - assert_match(/# config.file_watcher/, content) - end + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", + /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) end end - def test_rails_update_does_not_create_new_framework_defaults_by_default + def test_app_update_create_new_framework_defaults app_root = File.join(destination_root, "myapp") run_generator [app_root] - FileUtils.rm("#{app_root}/config/initializers/new_framework_defaults.rb") + assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb" stub_rails_application(app_root) do generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, destination_root: app_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/new_framework_defaults.rb" do |content| - assert_match(/ActiveSupport\.halt_callback_chains_on_return_false = true/, content) - assert_match(/Rails\.application\.config.active_record\.belongs_to_required_by_default = false/, content) - assert_no_match(/Rails\.application\.config\.ssl_options/, content) - end + assert_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb" end end - def test_rails_update_does_not_create_rack_cors + def test_app_update_does_not_create_rack_cors app_root = File.join(destination_root, "myapp") run_generator [app_root] @@ -230,7 +226,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_rails_update_does_not_remove_rack_cors_if_already_present + def test_app_update_does_not_remove_rack_cors_if_already_present app_root = File.join(destination_root, "myapp") run_generator [app_root] @@ -338,12 +334,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_file "config/environments/production.rb" do |content| assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + assert_match(/^ config\.read_encrypted_secrets = true/, content) end end def test_generator_defaults_to_puma_version run_generator [destination_root] - assert_gem "puma", "'~> 3.0'" + assert_gem "puma", "'~> 3.7'" end def test_generator_if_skip_puma_is_given @@ -356,15 +353,18 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_active_record_is_given run_generator [destination_root, "--skip-active-record"] + assert_no_directory "db/" assert_no_file "config/database.yml" assert_no_file "app/models/application_record.rb" assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ assert_file "test/test_helper.rb" do |helper_content| assert_no_match(/fixtures :all/, helper_content) end - - assert_file "config/initializers/new_framework_defaults.rb" do |initializer_content| - assert_no_match(/belongs_to_required_by_default/, initializer_content) + assert_file "bin/setup" do |setup_content| + assert_no_match(/db:setup/, setup_content) + end + assert_file "bin/update" do |update_content| + assert_no_match(/db:migrate/, update_content) end end @@ -398,7 +398,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) end assert_file "Gemfile" do |content| - assert_no_match(/jquery-rails/, content) assert_no_match(/sass-rails/, content) assert_no_match(/uglifier/, content) assert_no_match(/coffee-rails/, content) @@ -429,38 +428,61 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile", /^# gem 'redis'/ end + def test_generator_if_skip_test_is_given + run_generator [destination_root, "--skip-test"] + assert_file "Gemfile" do |content| + assert_no_match(/capybara/, content) + assert_no_match(/selenium-webdriver/, content) + end + end + + def test_generator_if_skip_system_test_is_given + run_generator [destination_root, "--skip_system_test"] + assert_file "Gemfile" do |content| + assert_no_match(/capybara/, content) + assert_no_match(/selenium-webdriver/, content) + end + end + + def test_does_not_generate_system_test_files_if_skip_system_test_is_given + run_generator [destination_root, "--skip_system_test"] + + Dir.chdir(destination_root) do + quietly { `./bin/rails g scaffold User` } + + assert_no_file("test/application_system_test_case.rb") + assert_no_file("test/system/users_test.rb") + end + end + + def test_generator_if_api_is_given + run_generator [destination_root, "--api"] + assert_file "Gemfile" do |content| + assert_no_match(/capybara/, content) + assert_no_match(/selenium-webdriver/, content) + end + end + def test_inclusion_of_javascript_runtime run_generator if defined?(JRUBY_VERSION) assert_gem "therubyrhino" else - assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/ + assert_file "Gemfile", /# gem 'mini_racer', platforms: :ruby/ end end - def test_jquery_is_the_default_javascript_library + def test_rails_ujs_is_the_default_ujs_library run_generator assert_file "app/assets/javascripts/application.js" do |contents| - assert_match %r{^//= require jquery}, contents - assert_match %r{^//= require jquery_ujs}, contents + assert_match %r{^//= require rails-ujs}, contents end - assert_gem "jquery-rails" - end - - def test_other_javascript_libraries - run_generator [destination_root, "-j", "prototype"] - assert_file "app/assets/javascripts/application.js" do |contents| - assert_match %r{^//= require prototype}, contents - assert_match %r{^//= require prototype_ujs}, contents - end - assert_gem "prototype-rails" end def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] assert_no_file "app/assets/javascripts" - assert_no_file "vendor/assets/javascripts" assert_file "app/views/layouts/application.html.erb" do |contents| assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents) @@ -469,7 +491,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_no_match(/coffee-rails/, content) - assert_no_match(/jquery-rails/, content) assert_no_match(/uglifier/, content) end @@ -478,6 +499,36 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_coffeescript_is_skipped_if_required + run_generator [destination_root, "--skip-coffee"] + + assert_file "Gemfile" do |content| + assert_no_match(/coffee-rails/, content) + assert_match(/uglifier/, content) + end + end + + def test_generator_for_yarn + run_generator([destination_root]) + assert_file "package.json", /dependencies/ + assert_file "config/initializers/assets.rb", /node_modules/ + end + + def test_generator_for_yarn_skipped + run_generator([destination_root, "--skip-yarn"]) + assert_no_file "package.json" + assert_no_file "bin/yarn" + + assert_file "config/initializers/assets.rb" do |content| + assert_no_match(/node_modules/, content) + end + + assert_file ".gitignore" do |content| + assert_no_match(/node_modules/, content) + assert_no_match(/yarn-error\.log/, content) + end + end + def test_inclusion_of_jbuilder run_generator assert_gem "jbuilder" @@ -512,9 +563,9 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator assert_file "config/environments/development.rb" do |content| if RbConfig::CONFIG["host_os"] =~ /darwin|linux/ - assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) else - assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) end end end @@ -560,6 +611,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_pretend_option output = run_generator [File.join(destination_root, "myapp"), "--pretend"] assert_no_match(/run bundle install/, output) + assert_no_match(/run git init/, output) end def test_application_name_with_spaces @@ -581,7 +633,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/\Agem 'web-console', '>= 3.3.0'\z/, content) + assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content) end end @@ -590,7 +642,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/\Agem 'web-console', '>= 3.3.0'\z/, content) + assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content) end end @@ -699,6 +751,11 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_version_control_initializes_git_repo + run_generator [destination_root] + assert_directory ".git" + end + def test_create_keeps run_generator folders_with_keep = %w( @@ -716,7 +773,6 @@ class AppGeneratorTest < Rails::Generators::TestCase test/helpers test/integration tmp - vendor/assets/stylesheets ) folders_with_keep.each do |folder| assert_file("#{folder}/.keep") @@ -725,7 +781,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_psych_gem run_generator - gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/ + gem_regex = /gem 'psych',\s+'~> 2\.0',\s+platforms: :rbx/ assert_file "Gemfile" do |content| if defined?(Rubinius) @@ -746,7 +802,7 @@ class AppGeneratorTest < Rails::Generators::TestCase template end - sequence = ["install", "exec spring binstub --all", "echo ran after_bundle"] + sequence = ["git init", "install", "exec spring binstub --all", "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}" @@ -761,11 +817,29 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - assert_equal 3, @sequence_step + assert_equal 4, @sequence_step + end + + def test_system_tests_directory_generated + run_generator + + assert_file("test/system/.keep") + assert_directory("test/system") end - protected + def test_system_tests_are_not_generated_on_system_test_skip + run_generator [destination_root, "--skip-system-test"] + + assert_no_directory("test/system") + end + + def test_system_tests_are_not_generated_on_test_skip + run_generator [destination_root, "--skip-test"] + + assert_no_directory("test/system") + end + private def stub_rails_application(root) Rails.application.config.root = root Rails.application.class.stub(:name, "Myapp") do @@ -790,7 +864,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "spring-watcher-listen" assert_file "config/environments/development.rb" do |content| - assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) end end @@ -800,7 +874,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_file "config/environments/development.rb" do |content| - assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) end end diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb index a1d54200ba..af68a9c49f 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -25,7 +25,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase end assert_file "app/assets/javascripts/channels/chat.js" do |channel| - assert_match(/App.chat = App.cable.subscriptions.create\("ChatChannel/, channel) + assert_match(/App\.chat = App\.cable\.subscriptions\.create\("ChatChannel/, channel) end end @@ -39,7 +39,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase end assert_file "app/assets/javascripts/channels/chat.js" do |channel| - assert_match(/App.chat = App.cable.subscriptions.create\("ChatChannel/, channel) + assert_match(/App\.chat = App\.cable\.subscriptions\.create\("ChatChannel/, channel) assert_match(/,\n\n speak/, channel) assert_match(/,\n\n mute: function\(\) \{\n return this\.perform\('mute'\);\n \}\n\}\);/, channel) end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 9b986a636a..af86a0136f 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -65,7 +65,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_add_routes run_generator - assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/ + assert_file "config/routes.rb", /^ get 'account\/foo'/, /^ get 'account\/bar'/ end def test_skip_routes diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb index ddd40e4d02..c7b0237f02 100644 --- a/railties/test/generators/create_migration_test.rb +++ b/railties/test/generators/create_migration_test.rb @@ -46,7 +46,7 @@ class CreateMigrationTest < Rails::Generators::TestCase def test_invoke create_migration - assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!) + assert_match(/create db\/migrate\/1_create_articles\.rb\n/, invoke!) assert_file @migration.destination end @@ -67,7 +67,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration - assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!) + assert_match(/identical db\/migrate\/1_create_articles\.rb\n/, invoke!) assert @migration.identical? end @@ -84,8 +84,8 @@ class CreateMigrationTest < Rails::Generators::TestCase create_migration(dest, force: true) { "different content" } stdout = invoke! - assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout) - assert_match(/create db\/migrate\/2_migration.rb\n/, stdout) + assert_match(/remove db\/migrate\/1_migration\.rb\n/, stdout) + assert_match(/create db\/migrate\/2_migration\.rb\n/, stdout) assert_file @migration.destination assert_no_file @existing_migration.destination end @@ -97,8 +97,8 @@ class CreateMigrationTest < Rails::Generators::TestCase end stdout = invoke! - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout) - assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, stdout) + assert_match(/create db\/migrate\/2_create_articles\.rb\n/, stdout) assert_no_file @migration.destination end @@ -106,7 +106,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration(default_destination_path, {}, skip: true) { "different content" } - assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!) + assert_match(/skip db\/migrate\/2_create_articles\.rb\n/, invoke!) assert_no_file @migration.destination end @@ -114,7 +114,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) assert_no_file @existing_migration.destination end @@ -122,13 +122,13 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration(default_destination_path, {}, pretend: true) - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) assert_file @existing_migration.destination end def test_revoke_when_no_exists create_migration - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) end end diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb new file mode 100644 index 0000000000..21fdcab19f --- /dev/null +++ b/railties/test/generators/encrypted_secrets_generator_test.rb @@ -0,0 +1,42 @@ +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/generator_test.rb b/railties/test/generators/generator_test.rb index c4e4747468..4444b3a56e 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -20,7 +20,7 @@ module Rails end def test_construction - klass = make_builder_class + klass = make_builder_class assert klass.start(["new", "blah"]) end @@ -88,12 +88,12 @@ module Rails specifier_for = -> v { generator.send(:rails_version_specifier, Gem::Version.new(v)) } assert_equal "~> 4.1.13", specifier_for["4.1.13"] - assert_equal [">= 4.1.6.rc1", "< 4.2"], specifier_for["4.1.6.rc1"] + assert_equal "~> 4.1.6.rc1", specifier_for["4.1.6.rc1"] assert_equal ["~> 4.1.7", ">= 4.1.7.1"], specifier_for["4.1.7.1"] assert_equal ["~> 4.1.7", ">= 4.1.7.1.2"], specifier_for["4.1.7.1.2"] - assert_equal [">= 4.1.7.1.rc2", "< 4.2"], specifier_for["4.1.7.1.rc2"] - assert_equal [">= 4.2.0.beta1", "< 4.3"], specifier_for["4.2.0.beta1"] - assert_equal [">= 5.0.0.beta1", "< 5.1"], specifier_for["5.0.0.beta1"] + assert_equal ["~> 4.1.7", ">= 4.1.7.1.rc2"], specifier_for["4.1.7.1.rc2"] + assert_equal "~> 4.2.0.beta1", specifier_for["4.2.0.beta1"] + assert_equal "~> 5.0.0.beta1", specifier_for["5.0.0.beta1"] end end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 2cdddc8713..5fb331e197 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -9,7 +9,7 @@ module Rails class << self remove_possible_method :root def root - @root ||= Pathname.new(File.expand_path("../../fixtures", __FILE__)) + @root ||= Pathname.new(File.expand_path("../fixtures", __dir__)) end end end @@ -41,7 +41,7 @@ module GeneratorsTestHelper end def copy_routes - routes = File.expand_path("../../../lib/rails/generators/rails/app/templates/config/routes.rb", __FILE__) + routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb", __dir__) destination = File.join(destination_root, "config") FileUtils.mkdir_p(destination) FileUtils.cp routes, destination diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb index 8bcc02440a..9358b63bd4 100644 --- a/railties/test/generators/integration_test_generator_test.rb +++ b/railties/test/generators/integration_test_generator_test.rb @@ -3,10 +3,14 @@ require "rails/generators/rails/integration_test/integration_test_generator" class IntegrationTestGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper - arguments %w(integration) def test_integration_test_skeleton_is_created - run_generator + run_generator %w(integration) assert_file "test/integration/integration_test.rb", /class IntegrationTest < ActionDispatch::IntegrationTest/ end + + def test_namespaced_integration_test_skeleton_is_created + run_generator %w(iguchi/integration) + assert_file "test/integration/iguchi/integration_test.rb", /class Iguchi::IntegrationTest < ActionDispatch::IntegrationTest/ + end end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 7d69d7470d..2ff03ea65e 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -9,13 +9,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase run_generator assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_match(/class NotifierMailer < ApplicationMailer/, mailer) - assert_no_match(/default from: "from@example.com"/, mailer) + assert_no_match(/default from: "from@example\.com"/, mailer) assert_no_match(/layout :mailer_notifier/, mailer) end assert_file "app/mailers/application_mailer.rb" do |mailer| assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) - assert_match(/default from: 'from@example.com'/, mailer) + assert_match(/default from: 'from@example\.com'/, mailer) assert_match(/layout 'mailer'/, mailer) end end @@ -48,11 +48,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/class NotifierMailerPreview < ActionMailer::Preview/, preview) assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/foo/, preview) assert_instance_method :foo, preview do |foo| - assert_match(/NotifierMailer.foo/, foo) + assert_match(/NotifierMailer\.foo/, foo) end assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/bar/, preview) assert_instance_method :bar, preview do |bar| - assert_match(/NotifierMailer.bar/, bar) + assert_match(/NotifierMailer\.bar/, bar) end end end @@ -137,12 +137,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_instance_method :foo, mailer do |foo| - assert_match(/mail to: "to@example.org"/, foo) + assert_match(/mail to: "to@example\.org"/, foo) assert_match(/@greeting = "Hi"/, foo) end assert_instance_method :bar, mailer do |bar| - assert_match(/mail to: "to@example.org"/, bar) + assert_match(/mail to: "to@example\.org"/, bar) assert_match(/@greeting = "Hi"/, bar) end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 6e1d1b70a9..6fe6e4ca07 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -204,8 +204,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/create_join_table :artists, :musics/, change) - assert_match(/# t.index \[:artist_id, :music_id\]/, change) - assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change) + assert_match(/# t\.index \[:artist_id, :music_id\]/, change) + assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change) end end end @@ -265,8 +265,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/create_join_table :artist, :music/, change) - assert_match(/# t.index \[:artist_id, :music_id\]/, change) - assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change) + assert_match(/# t\.index \[:artist_id, :music_id\]/, change) + assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change) end end end @@ -309,6 +309,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_to_configured_path + old_paths = Rails.application.config.paths["db/migrate"] + Rails.application.config.paths.add "db/migrate", with: "db2/migrate" + + migration = "migration_in_custom_path" + run_generator [migration] + assert_migration "db2/migrate/#{migration}.rb", /.*/ + ensure + Rails.application.config.paths["db/migrate"] = old_paths + end + private def with_singular_table_name diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 701d3ceaf2..f41969fc46 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -10,7 +10,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator assert_file "app/models/application_record.rb" do |record| assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) - assert_match(/self.abstract_class = true/, record) + assert_match(/self\.abstract_class = true/, record) end end @@ -212,14 +212,25 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_without_timestamps ActiveRecord::Base.timestamped_migrations = false run_generator ["account"] - assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ + assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ run_generator ["project"] - assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration\[[0-9.]+\]/ + assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration\[[0-9.]+\]/ ensure ActiveRecord::Base.timestamped_migrations = true end + def test_migration_with_configured_path + old_paths = Rails.application.config.paths["db/migrate"] + Rails.application.config.paths.add "db/migrate", with: "db2/migrate" + + run_generator + + assert_migration "db2/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ + ensure + Rails.application.config.paths["db/migrate"] = old_paths + end + def test_model_with_references_attribute_generates_belongs_to_associations run_generator ["product", "name:string", "supplier:references"] assert_file "app/models/product.rb", /belongs_to :supplier/ @@ -242,7 +253,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_timestamps run_generator - assert_migration "db/migrate/create_accounts.rb", /t.timestamps/ + assert_migration "db/migrate/create_accounts.rb", /t\.timestamps/ end def test_migration_timestamps_are_skipped @@ -250,7 +261,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_no_match(/t.timestamps/, up) + assert_no_match(/t\.timestamps/, up) end end end @@ -258,19 +269,19 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_is_skipped_with_skip_option run_generator output = run_generator ["Account", "--skip"] - assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_is_ignored_as_identical_with_skip_option run_generator ["Account"] output = run_generator ["Account", "--skip"] - assert_match %r{identical\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{identical\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_is_skipped_on_skip_behavior run_generator output = run_generator ["Account"], behavior: :skip - assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_error_is_not_shown_on_revoke @@ -300,7 +311,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "test/fixtures/accounts.yml", /name: MyString/, /age: 1/ assert_generated_fixture("test/fixtures/accounts.yml", - "one"=>{ "name"=>"MyString", "age"=>1 }, "two"=>{ "name"=>"MyString", "age"=>1 }) + "one" => { "name" => "MyString", "age" => 1 }, "two" => { "name" => "MyString", "age" => 1 }) end def test_fixtures_use_the_references_ids @@ -308,7 +319,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "test/fixtures/line_items.yml", /product: one\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - "one"=>{ "product"=>"one", "cart"=>"one" }, "two"=>{ "product"=>"two", "cart"=>"two" }) + "one" => { "product" => "one", "cart" => "one" }, "two" => { "product" => "two", "cart" => "two" }) end def test_fixtures_use_the_references_ids_and_type @@ -316,15 +327,15 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "test/fixtures/line_items.yml", /product: one\n product_type: Product\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - "one"=>{ "product"=>"one", "product_type"=>"Product", "cart"=>"one" }, - "two"=>{ "product"=>"two", "product_type"=>"Product", "cart"=>"two" }) + "one" => { "product" => "one", "product_type" => "Product", "cart" => "one" }, + "two" => { "product" => "two", "product_type" => "Product", "cart" => "two" }) end def test_fixtures_respect_reserved_yml_keywords run_generator ["LineItem", "no:integer", "Off:boolean", "ON:boolean"] assert_generated_fixture("test/fixtures/line_items.yml", - "one"=>{ "no"=>1, "Off"=>false, "ON"=>false }, "two"=>{ "no"=>1, "Off"=>false, "ON"=>false }) + "one" => { "no" => 1, "Off" => false, "ON" => false }, "two" => { "no" => 1, "Off" => false, "ON" => false }) end def test_fixture_is_skipped @@ -343,7 +354,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase ActiveRecord::Base.pluralize_table_names = false run_generator assert_generated_fixture("test/fixtures/account.yml", - "one"=>{ "name"=>"MyString", "age"=>1 }, "two"=>{ "name"=>"MyString", "age"=>1 }) + "one" => { "name" => "MyString", "age" => 1 }, "two" => { "name" => "MyString", "age" => 1 }) ensure ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 0258b3b9d7..3015b5363b 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -131,7 +131,7 @@ class NamedBaseTest < Rails::Generators::TestCase assert_name g, "admin.foos", :controller_i18n_scope end - protected + private def assert_name(generator, value, method) assert_equal value, generator.send(method) diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 15079f2735..af16a2641a 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,6 +1,7 @@ require "generators/generators_test_helper" require "rails/generators/rails/plugin/plugin_generator" require "generators/shared_generator_tests" +require "rails/engine/updater" DEFAULT_PLUGIN_FILES = %w( .gitignore @@ -47,7 +48,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [File.join(destination_root, "hyphenated-name")] assert_no_file "hyphenated-name/lib/hyphenated-name.rb" assert_no_file "hyphenated-name/lib/hyphenated_name.rb" - assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here...\n end\nend/ + assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here\.\.\.\n end\nend/ end def test_correct_file_in_lib_folder_of_camelcase_plugin_name @@ -100,6 +101,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_generating_adds_dummy_app_in_full_mode_without_sprockets + run_generator [destination_root, "-S", "--full"] + + assert_file "test/dummy/config/environments/production.rb" do |contents| + assert_no_match(/config\.assets/, contents) + end + end + def test_generating_adds_dummy_app_rake_tasks_without_unit_test_files run_generator [destination_root, "-T", "--mountable", "--dummy-path", "my_dummy_app"] assert_file "Rakefile", /APP_RAKEFILE/ @@ -143,7 +152,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-active-record"] assert_file "bukkits.gemspec" do |contents| - assert_no_match(/s.add_development_dependency "sqlite3"/, contents) + assert_no_match(/s\.add_development_dependency "sqlite3"/, contents) end end @@ -285,7 +294,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ - assert_file "hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/hyphenated\/name\/engine/ + assert_file "hyphenated-name/bin/rails", /\.\.\/lib\/hyphenated\/name\/engine/ end def test_creating_engine_with_hyphenated_and_underscored_name_in_full_mode @@ -299,14 +308,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/helpers" assert_file "my_hyphenated-name/app/mailers" assert_file "my_hyphenated-name/bin/rails" - assert_file "my_hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ + assert_file "my_hyphenated-name/config/routes.rb", /Rails\.application\.routes\.draw do/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ - assert_file "my_hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/my_hyphenated\/name\/engine/ + assert_file "my_hyphenated-name/bin/rails", /\.\.\/lib\/my_hyphenated\/name\/engine/ end def test_being_quiet_while_creating_dummy_application - assert_no_match(/create\s+config\/application.rb/, run_generator) + assert_no_match(/create\s+config\/application\.rb/, run_generator) end def test_create_mountable_application_with_mountable_option @@ -314,13 +323,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/javascripts/bukkits" assert_file "app/assets/stylesheets/bukkits" assert_file "app/assets/images/bukkits" - assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/ + assert_file "config/routes.rb", /Bukkits::Engine\.routes\.draw do/ assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/ assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/ assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/ assert_file "app/models/bukkits/application_record.rb", /module Bukkits\n class ApplicationRecord < ActiveRecord::Base/ assert_file "app/jobs/bukkits/application_job.rb", /module Bukkits\n class ApplicationJob < ActiveJob::Base/ - assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n/ + assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n/ assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/ assert_file "app/views/layouts/bukkits/application.html.erb" do |contents| assert_match "<title>Bukkits</title>", contents @@ -341,15 +350,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" - assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine.routes.draw do/ - assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0.1.0'\n end\nend/ + assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine\.routes\.draw do/ + assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/ assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ - assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ + assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/ assert_file "hyphenated-name/app/helpers/hyphenated/name/application_helper.rb", /module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ assert_file "hyphenated-name/app/views/layouts/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Hyphenated name</title>", contents @@ -363,15 +372,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" - assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine.routes.draw do/ - assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0.1.0'\n end\nend/ + assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine\.routes\.draw do/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/ assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ - assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ + assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/ assert_file "my_hyphenated-name/app/helpers/my_hyphenated/name/application_helper.rb", /module MyHyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ assert_file "my_hyphenated-name/app/views/layouts/my_hyphenated/name/application.html.erb" do |contents| assert_match "<title>My hyphenated name</title>", contents @@ -385,15 +394,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/stylesheets/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/images/deep/hyphenated/name" - assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine.routes.draw do/ - assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0.1.0'\n end\n end\nend/ + assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine\.routes\.draw do/ + assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/ assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/ assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/ assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ - assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/helpers/deep/hyphenated/name/application_helper.rb", /module Deep\n module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/views/layouts/deep/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Deep hyphenated name</title>", contents @@ -404,15 +413,16 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_gemspec run_generator - assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ - assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ - assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ + assert_file "bukkits.gemspec", /s\.name\s+= "bukkits"/ + assert_file "bukkits.gemspec", /s\.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ + assert_file "bukkits.gemspec", /s\.version\s+ = Bukkits::VERSION/ end def test_usage_of_engine_commands run_generator [destination_root, "--full"] - assert_file "bin/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/ - assert_file "bin/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/ + assert_file "bin/rails", /ENGINE_PATH = File\.expand_path\('\.\.\/lib\/bukkits\/engine', __dir__\)/ + assert_file "bin/rails", /ENGINE_ROOT = File\.expand_path\('\.\.', __dir__\)/ + assert_file "bin/rails", %r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)| assert_file "bin/rails", /require 'rails\/all'/ assert_file "bin/rails", /require 'rails\/engine\/commands'/ end @@ -459,7 +469,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator assert_file "test/dummy/config/environments/development.rb" do |contents| - assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents) + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents) end end @@ -482,6 +492,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_directory "test/dummy/doc" assert_no_directory "test/dummy/test" assert_no_directory "test/dummy/vendor" + assert_no_directory "test/dummy/.git" end def test_skipping_test_files @@ -499,7 +510,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_match("gemspec", contents) assert_match(/gem 'rails'/, contents) assert_match_sqlite3(contents) - assert_no_match(/# gem "jquery-rails"/, contents) end end @@ -527,6 +537,21 @@ class PluginGeneratorTest < Rails::Generators::TestCase FileUtils.rm gemfile_path end + def test_creating_plugin_only_specify_plugin_name_in_app_directory_adds_gemfile_entry + # simulate application existence + gemfile_path = "#{Rails.root}/Gemfile" + Object.const_set("APP_PATH", Rails.root) + FileUtils.touch gemfile_path + + FileUtils.cd(destination_root) + run_generator ["bukkits"] + + assert_file gemfile_path, /gem 'bukkits', path: 'bukkits'/ + ensure + Object.send(:remove_const, "APP_PATH") + FileUtils.rm gemfile_path + end + def test_skipping_gemfile_entry # simulate application existence gemfile_path = "#{Rails.root}/Gemfile" @@ -662,7 +687,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "#{destination_root}/app/models/bukkits/application_record.rb" do |record| assert_match(/module Bukkits/, record) assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) - assert_match(/self.abstract_class = true/, record) + assert_match(/self\.abstract_class = true/, record) end end @@ -707,7 +732,22 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end - protected + def test_app_update_generates_bin_file + run_generator [destination_root, "--mountable"] + + Object.const_set("ENGINE_ROOT", destination_root) + FileUtils.rm("#{destination_root}/bin/rails") + + quietly { Rails::Engine::Updater.run(:create_bin_files) } + + assert_file "#{destination_root}/bin/rails" do |content| + assert_match(%r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)|, content) + end + ensure + Object.send(:remove_const, "ENGINE_ROOT") + end + + private def action(*args, &block) silence(:stdout) { generator.send(*args, &block) } diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb index 04b4b10254..0bdd2a77d2 100644 --- a/railties/test/generators/plugin_test_runner_test.rb +++ b/railties/test/generators/plugin_test_runner_test.rb @@ -86,6 +86,23 @@ class PluginTestRunnerTest < ActiveSupport::TestCase assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) end + def test_executed_only_once + create_test_file "foo" + result = run_test_command("test/foo_test.rb") + assert_equal 1, result.scan(/1 runs, 1 assertions, 0 failures/).length + end + + def test_warnings_option + plugin_file "test/models/warnings_test.rb", <<-RUBY + require 'test_helper' + def test_warnings + a = 1 + end + RUBY + assert_match(/warning: assigned but unused variable/, + capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") }) + end + private def plugin_path "#{@destination_root}/bukkits" diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index bd23faf268..9971626f9f 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -230,6 +230,12 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_match(/@user\.destroy/, m) end end + + assert_no_file "app/views/users/index.html.erb" + assert_no_file "app/views/users/edit.html.erb" + assert_no_file "app/views/users/show.html.erb" + assert_no_file "app/views/users/new.html.erb" + assert_no_file "app/views/users/_form.html.erb" end def test_api_controller_tests diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 6b7e2c91d7..bc76cead18 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -62,6 +62,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) end + # System tests + assert_file "test/system/product_lines_test.rb" do |test| + assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, test) + end + # Views assert_no_file "app/views/layouts/product_lines.html.erb" @@ -432,8 +437,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/accounts/_form.html.erb" do |content| - assert_match(/^\W{4}<%= f\.text_field :name %>/, content) - assert_match(/^\W{4}<%= f\.text_field :currency_id %>/, 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) end end @@ -456,8 +461,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/users/_form.html.erb" do |content| - assert_match(/<%= f\.password_field :password %>/, content) - assert_match(/<%= f\.password_field :password_confirmation %>/, content) + assert_match(/<%= form\.password_field :password, id: :user_password %>/, content) + assert_match(/<%= form\.password_field :password_confirmation, id: :user_password_confirmation %>/, content) end assert_file "app/views/users/index.html.erb" do |content| @@ -492,6 +497,26 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end + def test_scaffold_tests_pass_by_default_inside_namespaced_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits-admin --mountable` } + + engine_path = File.join(destination_root, "bukkits-admin") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + + assert_file "bukkits-admin/app/controllers/bukkits/admin/users_controller.rb" do |content| + assert_match(/module Bukkits::Admin/, content) + assert_match(/class UsersController < ApplicationController/, content) + end + + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + def test_scaffold_tests_pass_by_default_inside_full_engine Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full` } @@ -533,4 +558,59 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end + + def test_scaffold_on_invoke_inside_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` } + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails generate scaffold User name:string age:integer` } + + assert File.exist?("app/models/bukkits/user.rb") + assert File.exist?("test/models/bukkits/user_test.rb") + assert File.exist?("test/fixtures/bukkits/users.yml") + + assert File.exist?("app/controllers/bukkits/users_controller.rb") + assert File.exist?("test/controllers/bukkits/users_controller_test.rb") + + assert File.exist?("app/views/bukkits/users/index.html.erb") + assert File.exist?("app/views/bukkits/users/edit.html.erb") + assert File.exist?("app/views/bukkits/users/show.html.erb") + assert File.exist?("app/views/bukkits/users/new.html.erb") + assert File.exist?("app/views/bukkits/users/_form.html.erb") + + assert File.exist?("app/helpers/bukkits/users_helper.rb") + + assert File.exist?("app/assets/javascripts/bukkits/users.js") + assert File.exist?("app/assets/stylesheets/bukkits/users.css") + end + end + + def test_scaffold_on_revoke_inside_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` } + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails generate scaffold User name:string age:integer` } + quietly { `bin/rails destroy scaffold User` } + + assert_not File.exist?("app/models/bukkits/user.rb") + assert_not File.exist?("test/models/bukkits/user_test.rb") + assert_not File.exist?("test/fixtures/bukkits/users.yml") + + assert_not File.exist?("app/controllers/bukkits/users_controller.rb") + assert_not File.exist?("test/controllers/bukkits/users_controller_test.rb") + + assert_not File.exist?("app/views/bukkits/users/index.html.erb") + assert_not File.exist?("app/views/bukkits/users/edit.html.erb") + assert_not File.exist?("app/views/bukkits/users/show.html.erb") + assert_not File.exist?("app/views/bukkits/users/new.html.erb") + assert_not File.exist?("app/views/bukkits/users/_form.html.erb") + + assert_not File.exist?("app/helpers/bukkits/users_helper.rb") + + assert_not File.exist?("app/assets/javascripts/bukkits/users.js") + assert_not File.exist?("app/assets/stylesheets/bukkits/users.css") + end + end end diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 27b2fc8955..5e75879964 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -28,7 +28,7 @@ module SharedGeneratorTests def test_plugin_new_generate_pretend run_generator ["testapp", "--pretend"] - default_files.each { |path| assert_no_file File.join("testapp",path) } + default_files.each { |path| assert_no_file File.join("testapp", path) } end def test_invalid_database_option_raises_an_error @@ -45,14 +45,14 @@ module SharedGeneratorTests reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| content = capture(:stderr) { run_generator [File.join(destination_root, reserved)] } - assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) + assert_match(/Invalid \w+ name #{reserved}\. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) end end def test_name_raises_an_error_if_name_already_used_constant %w{ String Hash Class Module Set Symbol }.each do |ruby_class| content = capture(:stderr) { run_generator [File.join(destination_root, ruby_class)] } - assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use. Please choose another \w+ name.\n/, content) + assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use\. Please choose another \w+ name\.\n/, content) end end @@ -109,6 +109,7 @@ module SharedGeneratorTests def test_skip_git run_generator [destination_root, "--skip-git", "--full"] assert_no_file(".gitignore") + assert_no_directory(".git") end def test_skip_keeps diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb new file mode 100644 index 0000000000..4622360244 --- /dev/null +++ b/railties/test/generators/system_test_generator_test.rb @@ -0,0 +1,17 @@ +require "generators/generators_test_helper" +require "rails/generators/rails/system_test/system_test_generator" + +class SystemTestGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + arguments %w(user) + + def test_system_test_skeleton_is_created + run_generator + assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/ + end + + def test_namespaced_system_test_skeleton_is_created + run_generator %w(admin/user) + assert_file "test/system/admin/users_test.rb", /class Admin::UsersTest < ApplicationSystemTestCase/ + end +end diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb index 4b5fb3ba3f..680dc2608e 100644 --- a/railties/test/generators/test_runner_in_engine_test.rb +++ b/railties/test/generators/test_runner_in_engine_test.rb @@ -17,7 +17,7 @@ class TestRunnerInEngineTest < ActiveSupport::TestCase create_test_file "post", pass: false output = run_test_command("test/post_test.rb") - expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/rails test test/post_test.rb:4} + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test\.rb:6\]:\nwups!\n\nbin/rails test test/post_test\.rb:4} assert_match expect, output end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 68ba435393..b784446535 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -200,7 +200,7 @@ class GeneratorsTest < Rails::Generators::TestCase self.class.class_eval(<<-end_eval, __FILE__, __LINE__ + 1) class WithOptionsGenerator < Rails::Generators::Base - class_option :generate, :default => true + class_option :generate, default: true, type: :boolean end end_eval @@ -233,7 +233,7 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_usage_with_embedded_ruby - require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", File.dirname(__FILE__)) + require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", __dir__) output = capture(:stdout) { Rails::Generators.invoke :usage_template, ["--help"] } assert_match(/:: 2 ::/, output) end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 8c1fe43a10..7496b5f84a 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -14,7 +14,7 @@ require "active_support/testing/autorun" require "active_support/testing/stream" require "active_support/test_case" -RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") +RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) # These files do not require any others and are needed # to run the tests @@ -22,6 +22,7 @@ require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" require "tmpdir" +require "rails/secrets" module TestHelpers module Paths @@ -104,7 +105,7 @@ module TestHelpers # Build an application by invoking the generator and going through the whole stack. def build_app(options = {}) @prev_rails_env = ENV["RAILS_ENV"] - ENV["RAILS_ENV"] = "development" + ENV["RAILS_ENV"] = "development" ENV["SECRET_KEY_BASE"] ||= SecureRandom.hex(16) FileUtils.rm_rf(app_path) @@ -118,7 +119,7 @@ module TestHelpers end routes = File.read("#{app_path}/config/routes.rb") - if routes =~ /(\n\s*end\s*)\Z/ + if routes =~ /(\n\s*end\s*)\z/ File.open("#{app_path}/config/routes.rb", "w") do |f| f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1 end @@ -162,7 +163,6 @@ module TestHelpers require "rails" require "action_controller/railtie" require "action_view/railtie" - require "action_dispatch/middleware/flash" @app = Class.new(Rails::Application) @app.config.eager_load = false @@ -187,7 +187,7 @@ module TestHelpers controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -251,7 +251,7 @@ module TestHelpers def add_to_config(str) environment = File.read("#{app_path}/config/application.rb") - if environment =~ /(\n\s*end\s*end\s*)\Z/ + if environment =~ /(\n\s*end\s*end\s*)\z/ File.open("#{app_path}/config/application.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end @@ -260,7 +260,7 @@ module TestHelpers def add_to_env_config(env, str) environment = File.read("#{app_path}/config/environments/#{env}.rb") - if environment =~ /(\n\s*end\s*)\Z/ + if environment =~ /(\n\s*end\s*)\z/ File.open("#{app_path}/config/environments/#{env}.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index 579e50ac95..c0b03d0c15 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -38,7 +38,7 @@ class PathGenerationTest < ActiveSupport::TestCase host = uri_or_host.host unless path path ||= uri_or_host.path - params = { "PATH_INFO" => path, + params = { "PATH_INFO" => path, "REQUEST_METHOD" => method, "HTTP_HOST" => host } diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 7b2551062a..f3db0a51d2 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -274,3 +274,23 @@ class PathsTest < ActiveSupport::TestCase end end end + +class PathsIntegrationTest < ActiveSupport::TestCase + test "A failed symlink is still a valid file" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + FileUtils.mkdir_p("foo") + File.symlink("foo/doesnotexist.rb", "foo/bar.rb") + assert_equal true, File.symlink?("foo/bar.rb") + + root = Rails::Paths::Root.new("foo") + root.add "bar.rb" + + exception = assert_raises(RuntimeError) do + root["bar.rb"].existent + end + assert_match File.expand_path("foo/bar.rb"), exception.message + end + end + end +end diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index 7dd91a2465..33b4bc6a3a 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -20,7 +20,7 @@ module Rails def development?; false; end end - class Subscriber < Struct.new(:starts, :finishes) + Subscriber = Struct.new(:starts, :finishes) do def initialize(starts = [], finishes = []) super end diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 59e79de41a..383adcc55d 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -39,7 +39,7 @@ class InfoTest < ActiveSupport::TestCase def test_rails_version assert_property "Rails version", - File.read(File.realpath("../../../RAILS_VERSION", __FILE__)).chomp + File.read(File.realpath("../../RAILS_VERSION", __dir__)).chomp end def test_html_includes_middleware @@ -54,7 +54,7 @@ class InfoTest < ActiveSupport::TestCase end end - protected + private def properties Rails::Info.properties end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 0c8896a715..e382a7a873 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -89,16 +89,16 @@ module RailtiesTest assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb") assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb") - assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output) - assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output) - assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output) + assert_match(/Copied migration 2_create_users\.bukkits\.rb from bukkits/, output) + assert_match(/Copied migration 3_add_last_name_to_users\.bukkits\.rb from bukkits/, output) + assert_match(/NOTE: Migration 3_create_sessions\.rb from bukkits has been skipped/, output) assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length output = `bundle exec rake railties:install:migrations`.split("\n") assert_no_match(/2_create_users/, output.join("\n")) - bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o }) + bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration 3_create_sessions\.rb from bukkits has been skipped/ =~ o }) assert_not_nil bukkits_migration_order, "Expected migration to be skipped" migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length @@ -133,10 +133,10 @@ module RailtiesTest boot_rails Dir.chdir(app_path) do - output = `bundle exec rake railties:install:migrations`.split("\n") + output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first) - assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last) + assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output.first) + assert_match(/Copied migration \d+_create_blogs\.blog_engine\.rb from blog_engine/, output.last) end end @@ -169,10 +169,10 @@ module RailtiesTest boot_rails Dir.chdir(app_path) do - output = `bundle exec rake railties:install:migrations`.split("\n") + output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users.core_engine.rb from core_engine/, output.first) - assert_match(/Copied migration \d+_create_keys.api_engine.rb from api_engine/, output.last) + assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first) + assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.last) end end @@ -202,7 +202,7 @@ module RailtiesTest Dir.chdir(@plugin.path) do output = `bundle exec rake app:bukkits:install:migrations` assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb") - assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output) + assert_match(/Copied migration 0_add_first_name_to_users\.bukkits\.rb from bukkits/, output) assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length end end @@ -327,7 +327,7 @@ module RailtiesTest controller "foo", <<-RUBY class FooController < ActionController::Base def index - render :text => "foo" + render plain: "foo" end end RUBY @@ -341,7 +341,7 @@ module RailtiesTest @plugin.write "app/controllers/bar_controller.rb", <<-RUBY class BarController < ActionController::Base def index - render :text => "bar" + render plain: "bar" end end RUBY @@ -436,7 +436,7 @@ YAML @plugin.write "app/controllers/admin/foo/bar_controller.rb", <<-RUBY class Admin::Foo::BarController < ApplicationController def index - render text: "Rendered from namespace" + render plain: "Rendered from namespace" end end RUBY @@ -536,7 +536,7 @@ YAML controller "foo", <<-RUBY class FooController < ActionController::Base def index - render text: params[:username] + render plain: params[:username] end end RUBY @@ -700,7 +700,7 @@ YAML end def show - render text: foo_path + render plain: foo_path end def from_app @@ -712,7 +712,7 @@ YAML end def polymorphic_path_without_namespace - render text: polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end end RUBY @@ -835,7 +835,7 @@ YAML @plugin.write "app/controllers/bukkits/awesome/foo_controller.rb", <<-RUBY class Bukkits::Awesome::FooController < ActionController::Base def index - render :text => "ok" + render plain: "ok" end end RUBY @@ -890,7 +890,7 @@ YAML boot_rails - assert_equal AppTemplate.railtie_namespace, AppTemplate::Engine + assert_equal AppTemplate::Engine, AppTemplate.railtie_namespace end test "properly reload routes" do @@ -1220,7 +1220,7 @@ YAML fullpath: \#{request.fullpath} path: \#{request.path} TEXT - render text: text + render plain: text end end end @@ -1257,7 +1257,7 @@ YAML app_file "app/controllers/bar_controller.rb", <<-RUBY class BarController < ApplicationController def index - render text: bukkits.bukkit_path + render plain: bukkits.bukkit_path end end RUBY @@ -1278,7 +1278,7 @@ YAML @plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY class Bukkits::BukkitController < ActionController::Base def index - render text: main_app.bar_path + render plain: main_app.bar_path end end RUBY @@ -1306,7 +1306,7 @@ YAML app_file "app/controllers/bar_controller.rb", <<-RUBY class BarController < ApplicationController def index - render text: bukkits.bukkit_path + render plain: bukkits.bukkit_path end end RUBY @@ -1327,7 +1327,7 @@ YAML @plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY class Bukkits::BukkitController < ActionController::Base def index - render text: main_app.bar_path + render plain: main_app.bar_path end end RUBY diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 732898d0c0..5c691b9ec4 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -29,7 +29,7 @@ module RailtiesTests `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end - def build_engine(is_mountable=false) + def build_engine(is_mountable = false) FileUtils.rm_rf(engine_path) FileUtils.mkdir_p(engine_path) diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb index 9db42c0c38..6639e55382 100644 --- a/railties/test/railties/mounted_engine_test.rb +++ b/railties/test/railties/mounted_engine_test.rb @@ -50,7 +50,7 @@ module ApplicationTests @simple_plugin.write "app/controllers/weblogs_controller.rb", <<-RUBY class WeblogsController < ActionController::Base def index - render text: request.url + render plain: request.url end end RUBY @@ -74,7 +74,7 @@ module ApplicationTests module Metrics class GeneratingController < ActionController::Base def generate_blog_route - render text: blog.post_path(1) + render plain: blog.post_path(1) end def generate_blog_route_in_view @@ -122,14 +122,14 @@ module ApplicationTests module Blog class PostsController < ActionController::Base def index - render text: blog.post_path(1) + render plain: blog.post_path(1) end def generate_application_route path = main_app.url_for(controller: "/main", action: "index", only_path: true) - render text: path + render plain: path end def application_route_in_view @@ -137,11 +137,11 @@ module ApplicationTests end def engine_polymorphic_path - render text: polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end def engine_asset_path - render inline: "<%= asset_path 'images/foo.png' %>" + render inline: "<%= asset_path 'images/foo.png', skip_pipeline: true %>" end end end @@ -150,7 +150,7 @@ module ApplicationTests app_file "app/controllers/application_generating_controller.rb", <<-RUBY class ApplicationGeneratingController < ActionController::Base def engine_route - render text: blog.posts_path + render plain: blog.posts_path end def engine_route_in_view @@ -158,7 +158,7 @@ module ApplicationTests end def weblog_engine_route - render text: weblog.weblogs_path + render plain: weblog.weblogs_path end def weblog_engine_route_in_view @@ -166,15 +166,15 @@ module ApplicationTests end def url_for_engine_route - render text: blog.url_for(controller: "blog/posts", action: "index", user: "john", only_path: true) + render plain: blog.url_for(controller: "blog/posts", action: "index", user: "john", only_path: true) end def polymorphic_route - render text: polymorphic_url([blog, Blog::Post.new]) + render plain: polymorphic_url([blog, Blog::Post.new]) end def application_polymorphic_path - render text: polymorphic_path(Blog::Post.new) + render plain: polymorphic_path(Blog::Post.new) end end RUBY diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb new file mode 100644 index 0000000000..36c8ef1fd9 --- /dev/null +++ b/railties/test/secrets_test.rb @@ -0,0 +1,124 @@ +require "abstract_unit" +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 + + test "setting read to false skips parsing" do + run_secrets_generator do + Rails::Secrets.write(<<-end_of_secrets) + test: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets + + Rails.application.config.read_encrypted_secrets = false + Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching 💃🕺 + assert_not Rails.application.secrets.yeah_yeah + end + end + + test "raises when reading secrets without a key" do + run_secrets_generator do + FileUtils.rm("config/secrets.yml.key") + + assert_raises Rails::Secrets::MissingKeyError do + Rails::Secrets.key + end + end + end + + test "reading with ENV variable" do + run_secrets_generator do + begin + old_key = ENV["RAILS_MASTER_KEY"] + ENV["RAILS_MASTER_KEY"] = IO.binread("config/secrets.yml.key").strip + FileUtils.rm("config/secrets.yml.key") + + assert_match "production:\n# external_api_key", Rails::Secrets.read + ensure + ENV["RAILS_MASTER_KEY"] = old_key + end + end + end + + test "reading from key file" do + run_secrets_generator do + File.binwrite("config/secrets.yml.key", "00112233445566778899aabbccddeeff") + + assert_equal "00112233445566778899aabbccddeeff", Rails::Secrets.key + end + end + + test "editing" do + run_secrets_generator do + decrypted_path = nil + + Rails::Secrets.read_for_editing do |tmp_path| + decrypted_path = tmp_path + + assert_match(/production:\n# external_api_key/, File.read(tmp_path)) + + File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") + end + + assert_not File.exist?(decrypted_path) + assert_equal "Empty streets, empty nights. The Downtown Lights.", Rails::Secrets.read + end + end + + test "merging secrets with encrypted precedence" do + run_secrets_generator do + File.write("config/secrets.yml", <<-end_of_secrets) + test: + yeah_yeah: lets-go-walking-down-this-empty-street + end_of_secrets + + Rails::Secrets.write(<<-end_of_secrets) + test: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets + + Rails.application.config.root = app_path + Rails.application.config.read_encrypted_secrets = true + Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching 💃🕺 + assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah + end + end + + test "refer secrets inside env config" do + run_secrets_generator do + Rails::Secrets.write(<<-end_of_yaml) + production: + some_secret: yeah yeah + end_of_yaml + + add_to_env_config "production", <<-end_of_config + config.dereferenced_secret = Rails.application.secrets.some_secret + end_of_config + + assert_equal "yeah yeah\n", `bin/rails runner -e production "puts Rails.application.config.dereferenced_secret"` + end + end + + private + def run_secrets_generator + Dir.chdir(app_path) do + capture(:stdout) do + Rails::Generators::EncryptedSecretsGenerator.start + end + + yield + end + end +end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index e22c939981..98201394cd 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -16,7 +16,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:\d+$}, @output.string + assert_match %r{^bin/rails test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string assert_rerun_snippet_count 1 end @@ -52,7 +52,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:\d+$}, @output.string + assert_match %r{^bin/test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string ensure Rails::TestUnitReporter.executable = original_executable end @@ -62,7 +62,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test\.rb:\d+\n\n\z} assert_match expect, @output.string end @@ -70,7 +70,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(errored_test) @reporter.report - expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test\.rb:\d+\n\n\z} assert_match expect, @output.string end @@ -79,7 +79,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase verbose.record(skipped_test) verbose.report - expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test\.rb:\d+\n\n\z} assert_match expect, @output.string end diff --git a/tasks/release.rb b/tasks/release.rb index d1717cec52..038fdc584a 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,11 +1,25 @@ FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable railties ) +FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") } -root = File.expand_path("../../", __FILE__) +root = File.expand_path("..", __dir__) version = File.read("#{root}/RAILS_VERSION").strip tag = "v#{version}" +gem_version = Gem::Version.new(version) directory "pkg" +# This "npm-ifies" the current version number +# With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its +# versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively. + +# "5.0.1" --> "5.0.1" +# "5.0.1.1" --> "5.0.1-1" * +# "5.0.0.rc1" --> "5.0.0-rc1" +# +# * This makes it a prerelease. That's bad, but we haven't come up with +# a better solution at the moment. +npm_version = version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s } + (FRAMEWORKS + ["rails"]).each do |framework| namespace framework do gem = "pkg/#{framework}-#{version}.gem" @@ -43,6 +57,17 @@ directory "pkg" raise "Could not insert PRE in #{file}" unless $1 File.open(file, "w") { |f| f.write ruby } + + require "json" + if File.exist?("#{framework}/package.json") && JSON.parse(File.read("#{framework}/package.json"))["version"] != npm_version + Dir.chdir("#{framework}") do + if sh "which npm" + sh "npm version #{npm_version} --no-git-tag-version" + else + raise "You must have npm installed to release Rails." + end + end + end end task gem => %w(update_versions pkg) do @@ -61,38 +86,10 @@ directory "pkg" task push: :build do sh "gem push #{gem}" - # When running the release task we usually run build first to check that the gem works properly. - # NPM will refuse to publish or rebuild the gem if the version is changed when the Rails gem - # versions are changed. This then causes the gem push to fail. Because of this we need to update - # the version and publish at the same time. if File.exist?("#{framework}/package.json") Dir.chdir("#{framework}") do - # This "npm-ifies" the current version - # With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its - # versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively. - - # In essence, the code below runs through all "."s that appear in the version, - # and checks to see if their index in the version string is greater than or equal to 2, - # and if so, it will change the "." to a "-". - - # Sample version transformations: - # irb(main):001:0> version = "5.0.1.1" - # => "5.0.1.1" - # irb(main):002:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } - # => "5.0.1-1" - # irb(main):003:0> version = "5.0.0.rc1" - # => "5.0.0.rc1" - # irb(main):004:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } - # => "5.0.0-rc1" - version = version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s } - - # Check if npm is installed, and raise an error if not - if sh "which npm" - sh "npm version #{version} --no-git-tag-version" - sh "npm publish" - else - raise "You must have npm installed to release Rails." - end + npm_tag = version =~ /[a-z]/ ? "pre" : "latest" + sh "npm publish --tag #{npm_tag}" end end end @@ -104,9 +101,11 @@ namespace :changelog do (FRAMEWORKS + ["guides"]).each do |fw| require "date" fname = File.join fw, "CHANGELOG.md" + current_contents = File.read(fname) - header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n* No changes.\n\n\n" - contents = header + File.read(fname) + header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n" + header << "* No changes.\n\n\n" if current_contents =~ /\A##/ + contents = header + current_contents File.open(fname, "wb") { |f| f.write contents } end end @@ -143,7 +142,7 @@ namespace :all do task push: FRAMEWORKS.map { |f| "#{f}:push" } + ["rails:push"] task :ensure_clean_state do - unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock'`.strip.empty? + unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock\\|package.json\\|version.rb'`.strip.empty? abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed" end @@ -158,14 +157,16 @@ namespace :all do end task :commit do - File.open("pkg/commit_message.txt", "w") do |f| - f.puts "# Preparing for #{version} release\n" - f.puts - f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT" - end + unless `git status -s`.strip.empty? + File.open("pkg/commit_message.txt", "w") do |f| + f.puts "# Preparing for #{version} release\n" + f.puts + f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT" + end - sh "git add . && git commit --verbose --template=pkg/commit_message.txt" - rm_f "pkg/commit_message.txt" + sh "git add . && git commit --verbose --template=pkg/commit_message.txt" + rm_f "pkg/commit_message.txt" + end end task :tag do @@ -173,7 +174,74 @@ namespace :all do sh "git push --tags" end - task prep_release: %w(ensure_clean_state build) + task prep_release: %w(ensure_clean_state build bundle commit) + + task release: %w(prep_release tag push) +end + +task :announce do + Dir.chdir("pkg/") do + if gem_version.segments[2] == 0 || gem_version.segments[3].is_a?(Integer) + # Not major releases, and not security releases + raise "Only valid for patch releases" + end + + sums = "$ shasum -a 256 *-#{version}.gem\n" + `shasum -a 256 *-#{version}.gem` - task release: %w(ensure_clean_state build bundle commit tag push) + puts "Hi everyone," + puts + + puts "I am happy to announce that Rails #{version} has been released." + puts + + previous_version = gem_version.segments[0, 3] + previous_version[2] -= 1 + previous_version = previous_version.join(".") + + if version =~ /rc/ + require "date" + future_date = Date.today + 5 + future_date += 1 while future_date.saturday? || future_date.sunday? + + github_user = `git config github.user`.chomp + + puts <<MSG +If no regressions are found, expect the final release on #{future_date.strftime('%A, %B %-d, %Y')}. +If you find one, please open an [issue on GitHub](https://github.com/rails/rails/issues/new) +#{"and mention me (@#{github_user}) on it, " unless github_user.empty?}so that we can fix it before the final release. + +MSG + end + + puts <<MSG +## CHANGES since #{previous_version} + +To view the changes for each gem, please read the changelogs on GitHub: + +MSG + FRAMEWORKS.sort.each do |framework| + puts "* [#{FRAMEWORK_NAMES[framework]} CHANGELOG](https://github.com/rails/rails/blob/v#{version}/#{framework}/CHANGELOG.md)" + end + puts <<MSG + +*Full listing* + +To see the full list of changes, [check out all the commits on +GitHub](https://github.com/rails/rails/compare/v#{previous_version}...v#{version}). + +## SHA-256 + +If you'd like to verify that your gem is the same as the one I've uploaded, +please use these SHA-256 hashes. + +Here are the checksums for #{version}: + +``` +#{sums} +``` + +As always, huge thanks to the many contributors who helped with this release. + +MSG + end end diff --git a/tools/profile b/tools/profile index f7d91e51cf..01e513c67a 100755 --- a/tools/profile +++ b/tools/profile @@ -12,7 +12,7 @@ module CodeTools Error = Class.new(StandardError) attr_reader :path, :mode - def initialize(path, mode=nil) + def initialize(path, mode = nil) assert_ruby_file_exists(path) @path, @mode = path, mode require "benchmark" diff --git a/tools/test.rb b/tools/test.rb index 7819c13ee2..1a1eca8f95 100644 --- a/tools/test.rb +++ b/tools/test.rb @@ -4,12 +4,18 @@ require "bundler" Bundler.setup require "rails/test_unit/minitest_plugin" +require "rails/test_unit/line_filtering" +require "active_support/test_case" -module Rails +class << Rails # Necessary to get rerun-snippts working. - def self.root + def root @root ||= Pathname.new(COMPONENT_ROOT) end + alias __root root end +ActiveSupport::TestCase.extend Rails::LineFiltering Rails::TestUnitReporter.executable = "bin/test" +Minitest.run_via = :rails +require "active_support/testing/autorun" diff --git a/version.rb b/version.rb index 9c49e0655a..7bacf2e0ba 100644 --- a/version.rb +++ b/version.rb @@ -6,7 +6,7 @@ module Rails module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" |